diff --git a/assets/themes/hailhydrant/hailhydrant.json b/assets/themes/hailhydrant/hailhydrant.json index d574854761..505bda2438 100644 --- a/assets/themes/hailhydrant/hailhydrant.json +++ b/assets/themes/hailhydrant/hailhydrant.json @@ -64,4 +64,4 @@ "ambulancestation" ], "widenFactor": 3 -} \ No newline at end of file +} diff --git a/scripts/downloadEli.ts b/scripts/downloadEli.ts index 8a56dd982c..2fa15f362b 100644 --- a/scripts/downloadEli.ts +++ b/scripts/downloadEli.ts @@ -13,10 +13,13 @@ class DownloadEli extends Script { const url = "https://osmlab.github.io/editor-layer-index/imagery.geojson" // Target should use '.json' instead of '.geojson', as the latter cannot be imported by the build systems const target = args[0] ?? "public/assets/data/editor-layer-index.json" + const targetGlobal = args[1] ?? "src/assets/generated/editor-layer-index-global.json" + const targetBing = args[0] ?? "src/assets/bing.json" const eli: Eli = await Utils.downloadJson(url) const keptLayers: EliEntry[] = [] + const keptGlobalLayers: EliEntry[] = [] console.log("Got", eli.features.length, "ELI-entries") for (let layer of eli.features) { const props = layer.properties @@ -95,18 +98,26 @@ class DownloadEli extends Script { } layer = { properties: layer.properties, type: layer.type, geometry: layer.geometry } - keptLayers.push(layer) + if(layer.geometry === null){ + keptGlobalLayers.push(layer) + }else{ + keptLayers.push(layer) + } } const contents = '{"type":"FeatureCollection",\n "features": [\n' + keptLayers - .filter((l) => l.properties.id !== "Bing") .map((l) => JSON.stringify(l)) .join(",\n") + "\n]}" - const bing = keptLayers.find((l) => l.properties.id === "Bing") + const contentsGlobal = + keptGlobalLayers + .filter((l) => l.properties.id !== "Bing") + .map(l => l.properties) + + const bing = keptGlobalLayers.find((l) => l.properties.id === "Bing") if (bing) { fs.writeFileSync(targetBing, JSON.stringify(bing), { encoding: "utf8" }) console.log("Written", targetBing) @@ -115,6 +126,9 @@ class DownloadEli extends Script { } fs.writeFileSync(target, contents, { encoding: "utf8" }) console.log("Written", keptLayers.length + ", entries to the ELI") + fs.writeFileSync(targetGlobal, JSON.stringify(contentsGlobal,null, " "), { encoding: "utf8" }) + console.log("Written", keptGlobalLayers.length + ", entries to the global ELI") + } } diff --git a/src/Models/RasterLayers.ts b/src/Models/RasterLayers.ts index cde3dddf34..c05d34bb86 100644 --- a/src/Models/RasterLayers.ts +++ b/src/Models/RasterLayers.ts @@ -1,5 +1,7 @@ import { Feature, Polygon } from "geojson" import * as globallayers from "../assets/global-raster-layers.json" +import * as globallayersEli from "../assets/generated/editor-layer-index-global.json" + import * as bingJson from "../assets/bing.json" import { BBox } from "../Logic/BBox" @@ -21,26 +23,37 @@ export class AvailableRasterLayers { } console.debug("Downloading ELI") const eli = await Utils.downloadJson<{ features: EditorLayerIndex }>( - "./assets/data/editor-layer-index.json" + "./assets/data/editor-layer-index.json", ) this._editorLayerIndex = eli.features?.filter((l) => l.properties.id !== "Bing") ?? [] this._editorLayerIndexStore.set(this._editorLayerIndex) return this._editorLayerIndex } - public static globalLayers: ReadonlyArray = globallayers.layers - .filter( - (properties) => - properties.id !== "osm.carto" && properties.id !== "Bing" /*Added separately*/ - ) - .map( + public static readonly globalLayers: ReadonlyArray = AvailableRasterLayers.initGlobalLayers() + + private static initGlobalLayers(): RasterLayerPolygon[] { + const gl: RasterLayerProperties[] = (globallayers["default"] ?? globallayers ).layers + .filter( + (properties) => + properties.id !== "osm.carto" && properties.id !== "Bing", /*Added separately*/ + ) + const glEli: RasterLayerProperties[] = globallayersEli["default"] ?? globallayersEli + const joined = gl.concat(glEli) + if (joined.some(j => !j.id)) { + console.log("Invalid layers:", JSON.stringify(joined .filter(l => !l.id))) + throw "Detected invalid global layer with invalid id" + } + return joined.map( (properties) => { type: "Feature", properties, geometry: BBox.global.asGeometry(), - } + }, ) + } + public static bing = bingJson public static readonly osmCartoProperties: RasterLayerProperties = { id: "osm", @@ -72,18 +85,18 @@ export class AvailableRasterLayers { public static layersAvailableAt( location: Store<{ lon: number; lat: number }>, - enableBing?: Store + enableBing?: Store, ): { store: Store } { const store = { store: undefined } Utils.AddLazyProperty(store, "store", () => - AvailableRasterLayers._layersAvailableAt(location, enableBing) + AvailableRasterLayers._layersAvailableAt(location, enableBing), ) return store } private static _layersAvailableAt( location: Store<{ lon: number; lat: number }>, - enableBing?: Store + enableBing?: Store, ): Store { this.editorLayerIndex() // start the download const availableLayersBboxes = Stores.ListStabilized( @@ -96,8 +109,8 @@ export class AvailableRasterLayers { const lonlat: [number, number] = [loc.lon, loc.lat] return eli.filter((eliPolygon) => BBox.get(eliPolygon).contains(lonlat)) }, - [AvailableRasterLayers._editorLayerIndexStore] - ) + [AvailableRasterLayers._editorLayerIndexStore], + ), ) return Stores.ListStabilized( availableLayersBboxes.map( @@ -119,15 +132,15 @@ export class AvailableRasterLayers { if ( !matching.some( (l) => - l.id === AvailableRasterLayers.defaultBackgroundLayer.properties.id + l.id === AvailableRasterLayers.defaultBackgroundLayer.properties.id, ) ) { matching.push(AvailableRasterLayers.defaultBackgroundLayer) } return matching }, - [enableBing] - ) + [enableBing], + ), ) } } @@ -146,7 +159,7 @@ export class RasterLayerUtils { available: RasterLayerPolygon[], preferredCategory: string, ignoreLayer?: RasterLayerPolygon, - skipLayers: number = 0 + skipLayers: number = 0, ): RasterLayerPolygon { const inCategory = available.filter((l) => l.properties.category === preferredCategory) const best: RasterLayerPolygon[] = inCategory.filter((l) => l.properties.best) @@ -154,7 +167,7 @@ export class RasterLayerUtils { let all = best.concat(others) console.log( "Selected layers are:", - all.map((l) => l.properties.id) + all.map((l) => l.properties.id), ) if (others.length > skipLayers) { all = all.slice(skipLayers) diff --git a/src/Models/ThemeConfig/Conversion/ConversionContext.ts b/src/Models/ThemeConfig/Conversion/ConversionContext.ts index ee38c8c62a..3f31103eb1 100644 --- a/src/Models/ThemeConfig/Conversion/ConversionContext.ts +++ b/src/Models/ThemeConfig/Conversion/ConversionContext.ts @@ -120,11 +120,11 @@ export class ConversionContext { return new ConversionContext(this.messages, this.path, [...this.operation, key]) } - warn(message: string) { - this.messages.push({ context: this, level: "warning", message }) + warn(...message: (string | number)[]) { + this.messages.push({ context: this, level: "warning", message: message.join(" ") }) } - err(...message: string[]) { + err(...message: (string | number)[]) { this._hasErrors = true this.messages.push({ context: this, level: "error", message: message.join(" ") }) } diff --git a/src/Models/ThemeConfig/Conversion/Validation.ts b/src/Models/ThemeConfig/Conversion/Validation.ts index 84f0463d49..6309d08661 100644 --- a/src/Models/ThemeConfig/Conversion/Validation.ts +++ b/src/Models/ThemeConfig/Conversion/Validation.ts @@ -20,6 +20,8 @@ import { Translatable } from "../Json/Translatable" import { ConversionContext } from "./ConversionContext" import PointRenderingConfigJson from "../Json/PointRenderingConfigJson" import { PrevalidateLayer } from "./PrevalidateLayer" +import { AvailableRasterLayers } from "../../RasterLayers" +import { eliCategory } from "../../RasterLayerProperties" export class ValidateLanguageCompleteness extends DesugaringStep { private readonly _languages: string[] @@ -28,7 +30,7 @@ export class ValidateLanguageCompleteness extends DesugaringStep { super( "Checks that the given object is fully translated in the specified languages", [], - "ValidateLanguageCompleteness" + "ValidateLanguageCompleteness", ) this._languages = languages ?? ["en"] } @@ -42,18 +44,18 @@ export class ValidateLanguageCompleteness extends DesugaringStep { .filter( (t) => t.tr.translations[neededLanguage] === undefined && - t.tr.translations["*"] === undefined + t.tr.translations["*"] === undefined, ) .forEach((missing) => { context .enter(missing.context.split(".")) .err( `The theme ${obj.id} should be translation-complete for ` + - neededLanguage + - ", but it lacks a translation for " + - missing.context + - ".\n\tThe known translation is " + - missing.tr.textFor("en") + neededLanguage + + ", but it lacks a translation for " + + missing.context + + ".\n\tThe known translation is " + + missing.tr.textFor("en"), ) }) } @@ -70,7 +72,7 @@ export class DoesImageExist extends DesugaringStep { constructor( knownImagePaths: Set, checkExistsSync: (path: string) => boolean = undefined, - ignore?: Set + ignore?: Set, ) { super("Checks if an image exists", [], "DoesImageExist") this._ignore = ignore @@ -103,22 +105,22 @@ export class DoesImageExist extends DesugaringStep { return image } - if(Utils.isEmoji(image)){ + if (Utils.isEmoji(image)) { return image } if (!this._knownImagePaths.has(image)) { if (this.doesPathExist === undefined) { context.err( - `Image with path ${image} not found or not attributed; it is used in ${context}` + `Image with path ${image} not found or not attributed; it is used in ${context}`, ) } else if (!this.doesPathExist(image)) { context.err( - `Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.` + `Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.`, ) } else { context.err( - `Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info` + `Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`, ) } } @@ -131,7 +133,7 @@ class OverrideShadowingCheck extends DesugaringStep { super( "Checks that an 'overrideAll' does not override a single override", [], - "OverrideShadowingCheck" + "OverrideShadowingCheck", ) } @@ -181,7 +183,7 @@ class MiscThemeChecks extends DesugaringStep { context .enter("layers") .err( - "The 'layers'-field should be an array, but it is not. Did you pase a layer identifier and forget to add the '[' and ']'?" + "The 'layers'-field should be an array, but it is not. Did you pase a layer identifier and forget to add the '[' and ']'?", ) } if (json.socialImage === "") { @@ -215,9 +217,25 @@ class MiscThemeChecks extends DesugaringStep { context .enter("overideAll") .err( - "'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them." + "'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them.", ) } + + if (json.defaultBackgroundId + && ![AvailableRasterLayers.osmCartoProperties.id, ...eliCategory ] + .find(l => l === json.defaultBackgroundId) ) { + const background = json.defaultBackgroundId + const match = AvailableRasterLayers.globalLayers.find(l => l.properties.id === background) + if (!match) { + const suggestions = Utils.sortedByLevenshteinDistance(background, + AvailableRasterLayers.globalLayers, l => l.properties.id) + context.enter("defaultBackgroundId") + .warn("The default background layer with id", background, "does not exist or is not a global layer. Perhaps you meant one of:", + suggestions.slice(0, 5).map(l => l.properties.id).join(", "), + "If you want to use a certain category of background image, use", AvailableRasterLayers.globalLayers.join(", ") + ) + } + } return json } } @@ -227,7 +245,7 @@ export class PrevalidateTheme extends Fuse { super( "Various consistency checks on the raw JSON", new MiscThemeChecks(), - new OverrideShadowingCheck() + new OverrideShadowingCheck(), ) } } @@ -237,7 +255,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep ["_abc"] */ private static extractCalculatedTagNames( - layerConfig?: LayerConfigJson | { calculatedTags: string[] } + layerConfig?: LayerConfigJson | { calculatedTags: string[] }, ) { return ( layerConfig?.calculatedTags?.map((ct) => { @@ -537,16 +555,16 @@ export class DetectShadowedMappings extends DesugaringStep does have `rel='noopener'` set", [], - "ValidatePossibleLinks" + "ValidatePossibleLinks", ) } @@ -601,21 +619,21 @@ export class ValidatePossibleLinks extends DesugaringStep, - context: ConversionContext + context: ConversionContext, ): string | Record { if (typeof json === "string") { if (this.isTabnabbingProne(json)) { context.err( "The string " + - json + - " has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping" + json + + " has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping", ) } } else { for (const k in json) { if (this.isTabnabbingProne(json[k])) { context.err( - `The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping` + `The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping`, ) } } @@ -633,7 +651,7 @@ export class CheckTranslation extends DesugaringStep { super( "Checks that a translation is valid and internally consistent", ["*"], - "CheckTranslation" + "CheckTranslation", ) this._allowUndefined = allowUndefined } @@ -680,7 +698,7 @@ export class ValidateLayerConfig extends DesugaringStep { isBuiltin: boolean, doesImageExist: DoesImageExist, studioValidations: boolean = false, - skipDefaultLayers: boolean = false + skipDefaultLayers: boolean = false, ) { super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig") this.validator = new ValidateLayer( @@ -688,7 +706,7 @@ export class ValidateLayerConfig extends DesugaringStep { isBuiltin, doesImageExist, studioValidations, - skipDefaultLayers + skipDefaultLayers, ) } @@ -716,7 +734,7 @@ export class ValidatePointRendering extends DesugaringStep { .enters("fields", i) .err( `Invalid filter: ${type} is not a valid textfield type.\n\tTry one of ${Array.from( - Validators.availableTypes - ).join(",")}` + Validators.availableTypes, + ).join(",")}`, ) } } @@ -893,13 +911,13 @@ export class DetectDuplicateFilters extends DesugaringStep<{ super( "Tries to detect layers where a shared filter can be used (or where similar filters occur)", [], - "DetectDuplicateFilters" + "DetectDuplicateFilters", ) } convert( json: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] }, - context: ConversionContext + context: ConversionContext, ): { layers: LayerConfigJson[]; themes: LayoutConfigJson[] } { const { layers, themes } = json const perOsmTag = new Map< @@ -963,7 +981,7 @@ export class DetectDuplicateFilters extends DesugaringStep<{ filter: FilterConfigJson }[] >, - layout?: LayoutConfigJson | undefined + layout?: LayoutConfigJson | undefined, ): void { if (layer.filter === undefined || layer.filter === null) { return @@ -1003,7 +1021,7 @@ export class DetectDuplicatePresets extends DesugaringStep { super( "Detects mappings which have identical (english) names or identical mappings.", ["presets"], - "DetectDuplicatePresets" + "DetectDuplicatePresets", ) } @@ -1014,13 +1032,13 @@ export class DetectDuplicatePresets extends DesugaringStep { if (new Set(enNames).size != enNames.length) { const dups = Utils.Duplicates(enNames) const layersWithDup = json.layers.filter((l) => - l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0) + l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0), ) const layerIds = layersWithDup.map((l) => l.id) context.err( `This theme has multiple presets which are named:${dups}, namely layers ${layerIds.join( - ", " - )} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets` + ", ", + )} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`, ) } @@ -1035,17 +1053,17 @@ export class DetectDuplicatePresets extends DesugaringStep { Utils.SameObject(presetATags, presetBTags) && Utils.sameList( presetA.preciseInput.snapToLayers, - presetB.preciseInput.snapToLayers + presetB.preciseInput.snapToLayers, ) ) { context.err( `This theme has multiple presets with the same tags: ${presetATags.asHumanString( false, false, - {} + {}, )}, namely the preset '${presets[i].title.textFor("en")}' and '${presets[ j - ].title.textFor("en")}'` + ].title.textFor("en")}'`, ) } } @@ -1070,13 +1088,13 @@ export class ValidateThemeEnsemble extends Conversion< super( "Validates that all themes together are logical, i.e. no duplicate ids exists within (overriden) themes", [], - "ValidateThemeEnsemble" + "ValidateThemeEnsemble", ) } convert( json: LayoutConfig[], - context: ConversionContext + context: ConversionContext, ): Map< string, { @@ -1127,11 +1145,11 @@ export class ValidateThemeEnsemble extends Conversion< context.err( [ "The layer with id '" + - id + - "' is found in multiple themes with different tag definitions:", + id + + "' is found in multiple themes with different tag definitions:", "\t In theme " + oldTheme + ":\t" + oldTags.asHumanString(false, false, {}), "\tIn theme " + theme.id + ":\t" + tags.asHumanString(false, false, {}), - ].join("\n") + ].join("\n"), ) } } diff --git a/src/UI/Image/UploadImage.svelte b/src/UI/Image/UploadImage.svelte index ff7d4581c6..594b17a706 100644 --- a/src/UI/Image/UploadImage.svelte +++ b/src/UI/Image/UploadImage.svelte @@ -62,7 +62,7 @@ return } - await state?.imageUploadManager.uploadImageAndApply(file, tags, targetKey) + await state?.imageUploadManager?.uploadImageAndApply(file, tags, targetKey) } catch (e) { console.error(e) state.reportError(e, "Could not upload image") diff --git a/src/assets/bing.json b/src/assets/bing.json index 0a75c21a38..e1f8ccaef7 100644 --- a/src/assets/bing.json +++ b/src/assets/bing.json @@ -1 +1 @@ -{"properties":{"name":"Bing Maps Aerial","id":"Bing","url":"https://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=14634&pr=odbl&n=f","type":"bing","category":"photo","min_zoom":1,"max_zoom":22},"type":"Feature","geometry":null} \ No newline at end of file +{"properties":{"name":"Bing Maps Aerial","id":"Bing","url":"https://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=14738&pr=odbl&n=f","type":"bing","category":"photo","min_zoom":1,"max_zoom":22},"type":"Feature","geometry":null} \ No newline at end of file