forked from MapComplete/MapComplete
		
	Favourites: include _all_ tagRenderings
This commit is contained in:
		
							parent
							
								
									eb444ab849
								
							
						
					
					
						commit
						473931891c
					
				
					 20 changed files with 436 additions and 154 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -6,6 +6,7 @@ scratch | |||
| assets/editor-layer-index.json | ||||
| assets/generated/* | ||||
| src/assets/generated/ | ||||
| assets/layers/favourite/favourite.json | ||||
| public/*.webmanifest | ||||
| /*.html | ||||
| !/index.html | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| { | ||||
|   "#":"no-translations", | ||||
|   "pointRendering": [ | ||||
|     { | ||||
|       "location": [ | ||||
|  | @ -37,35 +38,9 @@ | |||
|     "render": { | ||||
|       "en": "Favourite location", | ||||
|       "nl": "Favoriete locatie" | ||||
|     }, | ||||
|     "mappings": [ | ||||
|       { | ||||
|         "if": "name~*", | ||||
|         "then": { | ||||
|           "*": "{name}" | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|     } | ||||
|   }, | ||||
|   "tagRenderings": [ | ||||
|     { | ||||
|       "id": "Explanation", | ||||
|       "classes": "thanks", | ||||
|       "icon": { | ||||
|         "class": "large", | ||||
|         "path": "heart" | ||||
|       }, | ||||
|       "render": { | ||||
|         "en": "You marked this location as a personal favourite. As such, it is shown on every map you load.", | ||||
|         "nl": "Je hebt deze locatie als persoonlijke favoriet aangeduid en worden op alle MapComplete-kaarten getoond." | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "id": "show_images", | ||||
|       "render": { | ||||
|         "*": "{image_carousel()}" | ||||
|       } | ||||
|     }, | ||||
|     "all_tags" | ||||
|      | ||||
|   ] | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| { | ||||
|   "name": "mapcomplete", | ||||
|   "version": "0.35.0", | ||||
|   "version": "0.36.0", | ||||
|   "repository": "https://github.com/pietervdvn/MapComplete", | ||||
|   "description": "A small website to edit OSM easily", | ||||
|   "bugs": "https://github.com/pietervdvn/MapComplete/issues", | ||||
|  |  | |||
|  | @ -2,24 +2,242 @@ import Script from "./Script" | |||
| import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson" | ||||
| import { readFileSync, writeFileSync } from "fs" | ||||
| import { AllSharedLayers } from "../src/Customizations/AllSharedLayers" | ||||
| import { AllKnownLayoutsLazy } from "../src/Customizations/AllKnownLayouts" | ||||
| import { Utils } from "../src/Utils" | ||||
| import { AddEditingElements } from "../src/Models/ThemeConfig/Conversion/PrepareLayer" | ||||
| import { | ||||
|     MappingConfigJson, | ||||
|     QuestionableTagRenderingConfigJson, | ||||
| } from "../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" | ||||
| import { TagConfigJson } from "../src/Models/ThemeConfig/Json/TagConfigJson" | ||||
| import { TagUtils } from "../src/Logic/Tags/TagUtils" | ||||
| import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson" | ||||
| import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable" | ||||
| 
 | ||||
| export class GenerateFavouritesLayer extends Script { | ||||
|     private readonly layers: LayerConfigJson[] = [] | ||||
| 
 | ||||
| class PrepareFavouritesLayerJson extends Script { | ||||
|     constructor() { | ||||
|         super("Prepares the 'favourites'-layer") | ||||
|         const allThemes = new AllKnownLayoutsLazy(false).values() | ||||
|         for (const theme of allThemes) { | ||||
|             if (theme.hideFromOverview) { | ||||
|                 continue | ||||
|             } | ||||
|             for (const layer of theme.layers) { | ||||
|                 if (!layer.source) { | ||||
|                     continue | ||||
|                 } | ||||
|                 if (layer.source.geojsonSource) { | ||||
|                     continue | ||||
|                 } | ||||
|                 const layerConfig = AllSharedLayers.getSharedLayersConfigs().get(layer.id) | ||||
|                 if (!layerConfig) { | ||||
|                     continue | ||||
|                 } | ||||
|                 this.layers.push(layerConfig) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private addTagRenderings(proto: LayerConfigJson) { | ||||
|         const blacklistedIds = new Set([ | ||||
|             "images", | ||||
|             "questions", | ||||
|             "mapillary", | ||||
|             "leftover-questions", | ||||
|             "last_edit", | ||||
|             "minimap", | ||||
|             "move-button", | ||||
|             "delete-button", | ||||
|             "all-tags", | ||||
|             ...AddEditingElements.addedElements, | ||||
|         ]) | ||||
| 
 | ||||
|         const generateTagRenderings: (string | QuestionableTagRenderingConfigJson)[] = [] | ||||
|         const trPerId = new Map< | ||||
|             string, | ||||
|             { conditions: TagConfigJson[]; tr: QuestionableTagRenderingConfigJson } | ||||
|         >() | ||||
|         for (const layerConfig of this.layers) { | ||||
|             if (!layerConfig.tagRenderings) { | ||||
|                 continue | ||||
|             } | ||||
|             for (const tagRendering of layerConfig.tagRenderings) { | ||||
|                 if (typeof tagRendering === "string") { | ||||
|                     if (blacklistedIds.has(tagRendering)) { | ||||
|                         continue | ||||
|                     } | ||||
|                     generateTagRenderings.push(tagRendering) | ||||
|                     blacklistedIds.add(tagRendering) | ||||
|                     continue | ||||
|                 } | ||||
|                 if (tagRendering["builtin"]) { | ||||
|                     continue | ||||
|                 } | ||||
|                 const id = tagRendering.id | ||||
|                 if (blacklistedIds.has(id)) { | ||||
|                     continue | ||||
|                 } | ||||
|                 if (trPerId.has(id)) { | ||||
|                     const old = trPerId.get(id).tr | ||||
| 
 | ||||
|                     // We need to figure out if this was a 'recycled' tag rendering or just happens to have the same id
 | ||||
|                     function isSame(fieldName: string) { | ||||
|                         return old[fieldName]?.["en"] === tagRendering[fieldName]?.["en"] | ||||
|                     } | ||||
| 
 | ||||
|                     const sameQuestion = isSame("question") && isSame("render") | ||||
|                     if (!sameQuestion) { | ||||
|                         const newTr = <QuestionableTagRenderingConfigJson>Utils.Clone(tagRendering) | ||||
|                         newTr.id = layerConfig.id + "_" + newTr.id | ||||
|                         if (blacklistedIds.has(newTr.id)) { | ||||
|                             continue | ||||
|                         } | ||||
|                         newTr.condition = { | ||||
|                             and: Utils.NoNull([(newTr.condition, layerConfig.source["osmTags"])]), | ||||
|                         } | ||||
|                         generateTagRenderings.push(newTr) | ||||
|                         blacklistedIds.add(newTr.id) | ||||
|                         continue | ||||
|                     } | ||||
|                 } | ||||
|                 if (!trPerId.has(id)) { | ||||
|                     const newTr = <QuestionableTagRenderingConfigJson>Utils.Clone(tagRendering) | ||||
|                     generateTagRenderings.push(newTr) | ||||
|                     trPerId.set(newTr.id, { tr: newTr, conditions: [] }) | ||||
|                 } | ||||
|                 const conditions = trPerId.get(id).conditions | ||||
|                 if (tagRendering["condition"]) { | ||||
|                     conditions.push({ | ||||
|                         and: [tagRendering["condition"], layerConfig.source["osmTags"]], | ||||
|                     }) | ||||
|                 } else { | ||||
|                     conditions.push(layerConfig.source["osmTags"]) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         for (const { tr, conditions } of Array.from(trPerId.values())) { | ||||
|             const optimized = TagUtils.optimzeJson({ or: conditions }) | ||||
|             if (optimized === true) { | ||||
|                 continue | ||||
|             } | ||||
|             if (optimized === false) { | ||||
|                 throw "Optimized into 'false', this is weird..." | ||||
|             } | ||||
|             tr.condition = optimized | ||||
|         } | ||||
| 
 | ||||
|         const allTags: QuestionableTagRenderingConfigJson = { | ||||
|             id: "all-tags", | ||||
|             render: { "*": "{all_tags()}" }, | ||||
| 
 | ||||
|             metacondition: { | ||||
|                 or: [ | ||||
|                     "__featureSwitchIsDebugging=true", | ||||
|                     "mapcomplete-show_tags=full", | ||||
|                     "mapcomplete-show_debug=yes", | ||||
|                 ], | ||||
|             }, | ||||
|         } | ||||
|         proto.tagRenderings = [ | ||||
|             ...generateTagRenderings, | ||||
|             ...proto.tagRenderings, | ||||
|             "questions", | ||||
|             allTags, | ||||
|         ] | ||||
|     } | ||||
| 
 | ||||
|     private addTitle(proto: LayerConfigJson) { | ||||
|         const mappings: MappingConfigJson[] = [] | ||||
|         for (const layer of this.layers) { | ||||
|             const t = layer.title | ||||
|             const tags: TagConfigJson = layer.source["osmTags"] | ||||
|             if (!t) { | ||||
|                 continue | ||||
|             } | ||||
|             if (typeof t === "string") { | ||||
|                 mappings.push({ if: tags, then: t }) | ||||
|             } else if (t["render"] !== undefined || t["mappings"] !== undefined) { | ||||
|                 const tr = <TagRenderingConfigJson>t | ||||
|                 for (let i = 0; i < (tr.mappings ?? []).length; i++) { | ||||
|                     const mapping = tr.mappings[i] | ||||
|                     const optimized = TagUtils.optimzeJson({ | ||||
|                         and: [mapping.if, tags], | ||||
|                     }) | ||||
|                     if (optimized === false) { | ||||
|                         console.warn( | ||||
|                             "The following tags yielded 'false':", | ||||
|                             JSON.stringify(mapping.if), | ||||
|                             JSON.stringify(tags) | ||||
|                         ) | ||||
|                         continue | ||||
|                     } | ||||
|                     if (optimized === true) { | ||||
|                         console.error( | ||||
|                             "The following tags yielded 'false':", | ||||
|                             JSON.stringify(mapping.if), | ||||
|                             JSON.stringify(tags) | ||||
|                         ) | ||||
|                         throw "Tags for title optimized to true" | ||||
|                     } | ||||
| 
 | ||||
|                     if (!mapping.then) { | ||||
|                         throw ( | ||||
|                             "The title has a missing 'then' for mapping " + | ||||
|                             i + | ||||
|                             " in layer " + | ||||
|                             layer.id | ||||
|                         ) | ||||
|                     } | ||||
|                     mappings.push({ | ||||
|                         if: optimized, | ||||
|                         then: mapping.then, | ||||
|                     }) | ||||
|                 } | ||||
|                 if (tr.render) { | ||||
|                     mappings.push({ | ||||
|                         if: tags, | ||||
|                         then: <Translatable>tr.render, | ||||
|                     }) | ||||
|                 } | ||||
|             } else { | ||||
|                 mappings.push({ if: tags, then: <Record<string, string>>t }) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (proto.title["mappings"]) { | ||||
|             mappings.unshift(...proto.title["mappings"]) | ||||
|         } | ||||
|         if (proto.title["render"]) { | ||||
|             mappings.push({ | ||||
|                 if: "id~*", | ||||
|                 then: proto.title["render"], | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         proto.title = { | ||||
|             mappings, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async main(args: string[]): Promise<void> { | ||||
|         const allConfigs = AllSharedLayers.getSharedLayersConfigs() | ||||
|         console.log("Generating the favourite layer: stealing _all_ tagRenderings") | ||||
|         const proto = this.readLayer("favourite/favourite.proto.json") | ||||
|         const questions = allConfigs.get("questions") | ||||
|         proto.tagRenderings.push(...questions.tagRenderings) | ||||
| 
 | ||||
|         this.addTagRenderings(proto) | ||||
|         this.addTitle(proto) | ||||
|         writeFileSync("./assets/layers/favourite/favourite.json", JSON.stringify(proto, null, "  ")) | ||||
|     } | ||||
| 
 | ||||
|     private readLayer(path: string): LayerConfigJson { | ||||
|         return JSON.parse(readFileSync("./assets/layers/" + path, "utf8")) | ||||
|         try { | ||||
|             return JSON.parse(readFileSync("./assets/layers/" + path, "utf8")) | ||||
|         } catch (e) { | ||||
|             console.error("Could not read ./assets/layers/" + path) | ||||
|             throw e | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| new PrepareFavouritesLayerJson().run() | ||||
| new GenerateFavouritesLayer().run() | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ import { QuestionableTagRenderingConfigJson } from "../src/Models/ThemeConfig/Js | |||
| import LayerConfig from "../src/Models/ThemeConfig/LayerConfig" | ||||
| import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig" | ||||
| import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext" | ||||
| import { GenerateFavouritesLayer } from "./generateFavouritesLayer" | ||||
| 
 | ||||
| // This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
 | ||||
| // It spits out an overview of those to be used to load them
 | ||||
|  | @ -381,16 +382,11 @@ class LayerOverviewUtils extends Script { | |||
|             forceReload | ||||
|         ) | ||||
| 
 | ||||
|         writeFileSync( | ||||
|             "./src/assets/generated/known_themes.json", | ||||
|             JSON.stringify({ | ||||
|                 themes: Array.from(sharedThemes.values()), | ||||
|             }) | ||||
|         ) | ||||
| 
 | ||||
|         writeFileSync( | ||||
|             "./src/assets/generated/known_layers.json", | ||||
|             JSON.stringify({ layers: Array.from(sharedLayers.values()) }) | ||||
|             JSON.stringify({ | ||||
|                 layers: Array.from(sharedLayers.values()).filter((l) => l.id !== "favourite"), | ||||
|             }) | ||||
|         ) | ||||
| 
 | ||||
|         const mcChangesPath = "./assets/themes/mapcomplete-changes/mapcomplete-changes.json" | ||||
|  | @ -428,6 +424,19 @@ class LayerOverviewUtils extends Script { | |||
|             ConversionContext.construct([], []) | ||||
|         ) | ||||
| 
 | ||||
|         for (const [_, theme] of sharedThemes) { | ||||
|             theme.layers = theme.layers.filter( | ||||
|                 (l) => Constants.added_by_default.indexOf(l["id"]) < 0 | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         writeFileSync( | ||||
|             "./src/assets/generated/known_themes.json", | ||||
|             JSON.stringify({ | ||||
|                 themes: Array.from(sharedThemes.values()), | ||||
|             }) | ||||
|         ) | ||||
| 
 | ||||
|         const end = new Date() | ||||
|         const millisNeeded = end.getTime() - start.getTime() | ||||
|         if (AllSharedLayers.getSharedLayersConfigs().size == 0) { | ||||
|  | @ -791,4 +800,5 @@ class LayerOverviewUtils extends Script { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| new GenerateFavouritesLayer().run() | ||||
| new LayerOverviewUtils().run() | ||||
|  |  | |||
|  | @ -1,45 +1,54 @@ | |||
| import known_themes from "../assets/generated/known_themes.json" | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | ||||
| import favourite from "../assets/generated/layers/favourite.json" | ||||
| import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" | ||||
| import { AllSharedLayers } from "./AllSharedLayers" | ||||
| import Constants from "../Models/Constants" | ||||
| 
 | ||||
| /** | ||||
|  * Somewhat of a dictionary, which lazily parses needed themes | ||||
|  */ | ||||
| export class AllKnownLayoutsLazy { | ||||
|     private readonly dict: Map<string, { data: LayoutConfig } | { func: () => LayoutConfig }> = | ||||
|         new Map() | ||||
|     constructor() { | ||||
|     private readonly raw: Map<string, LayoutConfigJson> = new Map() | ||||
|     private readonly dict: Map<string, LayoutConfig> = new Map() | ||||
| 
 | ||||
|     constructor(includeFavouriteLayer = true) { | ||||
|         for (const layoutConfigJson of known_themes["themes"]) { | ||||
|             this.dict.set(layoutConfigJson.id, { | ||||
|                 func: () => { | ||||
|                     const layout = new LayoutConfig(<LayoutConfigJson>layoutConfigJson, true) | ||||
|                     for (let i = 0; i < layout.layers.length; i++) { | ||||
|                         let layer = layout.layers[i] | ||||
|                         if (typeof layer === "string") { | ||||
|                             throw "Layer " + layer + " was not expanded in " + layout.id | ||||
|                         } | ||||
|             for (const layerId of Constants.added_by_default) { | ||||
|                 if (layerId === "favourite") { | ||||
|                     if (includeFavouriteLayer) { | ||||
|                         layoutConfigJson.layers.push(favourite) | ||||
|                     } | ||||
|                     return layout | ||||
|                 }, | ||||
|             }) | ||||
|                     continue | ||||
|                 } | ||||
|                 const defaultLayer = AllSharedLayers.getSharedLayersConfigs().get(layerId) | ||||
|                 if (defaultLayer === undefined) { | ||||
|                     console.error("Could not find builtin layer", layerId) | ||||
|                     continue | ||||
|                 } | ||||
|                 layoutConfigJson.layers.push(defaultLayer) | ||||
|             } | ||||
|             this.raw.set(layoutConfigJson.id, layoutConfigJson) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public getConfig(key: string): LayoutConfigJson { | ||||
|         return this.raw.get(key) | ||||
|     } | ||||
| 
 | ||||
|     public get(key: string): LayoutConfig { | ||||
|         const thunk = this.dict.get(key) | ||||
|         if (thunk === undefined) { | ||||
|             return undefined | ||||
|         const cached = this.dict.get(key) | ||||
|         if (cached !== undefined) { | ||||
|             return cached | ||||
|         } | ||||
|         if (thunk["data"]) { | ||||
|             return thunk["data"] | ||||
|         } | ||||
|         const layout = thunk["func"]() | ||||
|         this.dict.set(key, { data: layout }) | ||||
| 
 | ||||
|         const layout = new LayoutConfig(this.getConfig(key)) | ||||
|         this.dict.set(key, layout) | ||||
|         return layout | ||||
|     } | ||||
| 
 | ||||
|     public keys() { | ||||
|         return this.dict.keys() | ||||
|         return this.raw.keys() | ||||
|     } | ||||
| 
 | ||||
|     public values() { | ||||
|  |  | |||
|  | @ -173,7 +173,6 @@ export default class GeoLocationHandler { | |||
|                     properties[k] = location[k] | ||||
|                 } | ||||
|             } | ||||
|             console.debug("Current location object:", location) | ||||
|             properties["_all"] = JSON.stringify(location) | ||||
| 
 | ||||
|             const feature = <Feature>{ | ||||
|  |  | |||
|  | @ -16,6 +16,11 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { | |||
|     private readonly _osmConnection: OsmConnection | ||||
|     private readonly _detectedIds: Store<string[]> | ||||
| 
 | ||||
|     /** | ||||
|      * All favourites, including the ones which are filtered away because they are already displayed | ||||
|      */ | ||||
|     public readonly allFavourites: Store<Feature[]> | ||||
| 
 | ||||
|     constructor( | ||||
|         connection: OsmConnection, | ||||
|         indexedSource: FeaturePropertiesStore, | ||||
|  | @ -53,6 +58,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { | |||
|         ) | ||||
| 
 | ||||
|         super(featuresWithoutAlreadyPresent) | ||||
|         this.allFavourites = features | ||||
| 
 | ||||
|         this._osmConnection = connection | ||||
|         this._detectedIds = Stores.ListStabilized( | ||||
|  | @ -76,6 +82,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { | |||
|         const geometry = <[number, number]>JSON.parse(prefs[key]) | ||||
|         const properties = FavouritesFeatureSource.getPropertiesFor(prefs, id) | ||||
|         properties._orig_layer = prefs[FavouritesFeatureSource.prefix + id + "-layer"] | ||||
|         properties._orig_theme = prefs[FavouritesFeatureSource.prefix + id + "-theme"] | ||||
| 
 | ||||
|         properties.id = osmId | ||||
|         properties._favourite = "yes" | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { LayerConfigJson } from "../Json/LayerConfigJson" | |||
| import { Utils } from "../../../Utils" | ||||
| import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" | ||||
| import { ConversionContext } from "./ConversionContext" | ||||
| import { T } from "vitest/dist/types-aac763a5" | ||||
| 
 | ||||
| export interface DesugaringContext { | ||||
|     tagRenderings: Map<string, QuestionableTagRenderingConfigJson> | ||||
|  | @ -81,18 +82,36 @@ export class Pure<TIn, TOut> extends Conversion<TIn, TOut> { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| export class Bypass<T> extends DesugaringStep<T> { | ||||
|     private readonly _applyIf: (t: T) => boolean | ||||
|     private readonly _step: DesugaringStep<T> | ||||
|     constructor(applyIf: (t: T) => boolean, step: DesugaringStep<T>) { | ||||
|         super("Applies the step on the object, if the object satisfies the predicate", [], "Bypass") | ||||
|         this._applyIf = applyIf | ||||
|         this._step = step | ||||
|     } | ||||
| 
 | ||||
|     convert(json: T, context: ConversionContext): T { | ||||
|         if (!this._applyIf(json)) { | ||||
|             return json | ||||
|         } | ||||
|         return this._step.convert(json, context) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class Each<X, Y> extends Conversion<X[], Y[]> { | ||||
|     private readonly _step: Conversion<X, Y> | ||||
|     private readonly _msg: string | ||||
|     private readonly _filter: (x: X) => boolean | ||||
| 
 | ||||
|     constructor(step: Conversion<X, Y>, msg?: string) { | ||||
|     constructor(step: Conversion<X, Y>, options?: { msg?: string }) { | ||||
|         super( | ||||
|             "Applies the given step on every element of the list", | ||||
|             [], | ||||
|             "OnEach(" + step.name + ")" | ||||
|         ) | ||||
|         this._step = step | ||||
|         this._msg = msg | ||||
|         this._msg = options?.msg | ||||
|     } | ||||
| 
 | ||||
|     convert(values: X[], context: ConversionContext): Y[] { | ||||
|  |  | |||
|  | @ -85,7 +85,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L | |||
|             description: trs(t.description, { title: layer.title.render }), | ||||
|             source: { | ||||
|                 osmTags: { | ||||
|                     and: ["id~*"], | ||||
|                     and: ["id~[0-9]+", "comment_url~.*notes/[0-9]*g.json"], | ||||
|                 }, | ||||
|                 geoJson: | ||||
|                     "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=" + | ||||
|  |  | |||
|  | @ -566,6 +566,16 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> { | |||
| } | ||||
| 
 | ||||
| export class AddEditingElements extends DesugaringStep<LayerConfigJson> { | ||||
|     static addedElements: string[] = [ | ||||
|         "minimap", | ||||
|         "just_created", | ||||
|         "split_button", | ||||
|         "move_button", | ||||
|         "delete_button", | ||||
|         "last_edit", | ||||
|         "favourite_state", | ||||
|         "all_tags", | ||||
|     ] | ||||
|     private readonly _desugaring: DesugaringContext | ||||
| 
 | ||||
|     constructor(desugaring: DesugaringContext) { | ||||
|  | @ -1210,7 +1220,7 @@ class AddFavouriteBadges extends DesugaringStep<LayerConfigJson> { | |||
|     } | ||||
| 
 | ||||
|     convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson { | ||||
|         if (json.id === "favourite") { | ||||
|         if (json.source === "special" || json.source === "special:library") { | ||||
|             return json | ||||
|         } | ||||
|         const pr = json.pointRendering?.[0] | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { Conversion, DesugaringStep, Each, Fuse, On, Pipe, Pure } from "./Conversion" | ||||
| import { Bypass, Conversion, DesugaringStep, Each, Fuse, On } from "./Conversion" | ||||
| import { LayerConfigJson } from "../Json/LayerConfigJson" | ||||
| import LayerConfig from "../LayerConfig" | ||||
| import { Utils } from "../../../Utils" | ||||
|  | @ -11,7 +11,6 @@ import { TagUtils } from "../../../Logic/Tags/TagUtils" | |||
| import { ExtractImages } from "./FixImages" | ||||
| import { And } from "../../../Logic/Tags/And" | ||||
| import Translations from "../../../UI/i18n/Translations" | ||||
| import Svg from "../../../Svg" | ||||
| import FilterConfigJson from "../Json/FilterConfigJson" | ||||
| import DeleteConfig from "../DeleteConfig" | ||||
| import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" | ||||
|  | @ -276,9 +275,9 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> { | |||
|             new On( | ||||
|                 "layers", | ||||
|                 new Each( | ||||
|                     new Pipe( | ||||
|                         new ValidateLayer(undefined, isBuiltin, doesImageExist, false, true), | ||||
|                         new Pure((x) => x?.raw) | ||||
|                     new Bypass( | ||||
|                         (layer) => Constants.added_by_default.indexOf(<any>layer.id) < 0, | ||||
|                         new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true) | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|  | @ -968,7 +967,7 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> { | |||
|             "Various validation on tagRenderingConfigs", | ||||
|             new DetectShadowedMappings(layerConfig), | ||||
|             new DetectConflictingAddExtraTags(), | ||||
|             //    new DetectNonErasedKeysInMappings(),
 | ||||
|             // TODO enable   new DetectNonErasedKeysInMappings(),
 | ||||
|             new DetectMappingsWithImages(doesImageExist), | ||||
|             new On("render", new ValidatePossibleLinks()), | ||||
|             new On("question", new ValidatePossibleLinks()), | ||||
|  | @ -1350,6 +1349,29 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> { | ||||
|     private readonly validator: ValidateLayer | ||||
|     constructor( | ||||
|         path: string, | ||||
|         isBuiltin: boolean, | ||||
|         doesImageExist: DoesImageExist, | ||||
|         studioValidations: boolean = false, | ||||
|         skipDefaultLayers: boolean = false | ||||
|     ) { | ||||
|         super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig") | ||||
|         this.validator = new ValidateLayer( | ||||
|             path, | ||||
|             isBuiltin, | ||||
|             doesImageExist, | ||||
|             studioValidations, | ||||
|             skipDefaultLayers | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson { | ||||
|         return this.validator.convert(json, context).raw | ||||
|     } | ||||
| } | ||||
| export class ValidateLayer extends Conversion< | ||||
|     LayerConfigJson, | ||||
|     { parsed: LayerConfig; raw: LayerConfigJson } | ||||
|  |  | |||
|  | @ -462,6 +462,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|      * @private | ||||
|      */ | ||||
|     private selectClosestAtCenter(i: number = 0) { | ||||
|         this.mapProperties.lastKeyNavigation.setData(Date.now() / 1000) | ||||
|         const toSelect = this.closestFeatures.features.data[i] | ||||
|         if (!toSelect) { | ||||
|             return | ||||
|  | @ -567,46 +568,6 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private addLastClick(last_click: LastClickFeatureSource) { | ||||
|         // The last_click gets a _very_ special treatment as it interacts with various parts
 | ||||
| 
 | ||||
|         this.featureProperties.trackFeatureSource(last_click) | ||||
|         this.indexedFeatures.addSource(last_click) | ||||
| 
 | ||||
|         last_click.features.addCallbackAndRunD((features) => { | ||||
|             if (this.selectedLayer.data?.id === "last_click") { | ||||
|                 // The last-click location moved, but we have selected the last click of the previous location
 | ||||
|                 // So, we update _after_ clearing the selection to make sure no stray data is sticking around
 | ||||
|                 this.selectedElement.setData(undefined) | ||||
|                 this.selectedElement.setData(features[0]) | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         new ShowDataLayer(this.map, { | ||||
|             features: new FilteringFeatureSource(this.newPointDialog, last_click), | ||||
|             doShowLayer: this.featureSwitches.featureSwitchEnableLogin, | ||||
|             layer: this.newPointDialog.layerDef, | ||||
|             selectedElement: this.selectedElement, | ||||
|             selectedLayer: this.selectedLayer, | ||||
|             metaTags: this.userRelatedState.preferencesAsTags, | ||||
|             onClick: (feature: Feature) => { | ||||
|                 if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) { | ||||
|                     this.map.data.flyTo({ | ||||
|                         zoom: Constants.minZoomLevelToAddNewPoint, | ||||
|                         center: this.mapProperties.lastClickLocation.data, | ||||
|                     }) | ||||
|                     return | ||||
|                 } | ||||
|                 // We first clear the selection to make sure no weird state is around
 | ||||
|                 this.selectedLayer.setData(undefined) | ||||
|                 this.selectedElement.setData(undefined) | ||||
| 
 | ||||
|                 this.selectedElement.setData(feature) | ||||
|                 this.selectedLayer.setData(this.newPointDialog.layerDef) | ||||
|             }, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add the special layers to the map | ||||
|      */ | ||||
|  | @ -663,9 +624,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         } | ||||
| 
 | ||||
|         const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range") | ||||
| 
 | ||||
|         const rangeIsDisplayed = rangeFLayer?.isDisplayed | ||||
| 
 | ||||
|         if ( | ||||
|             !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef)) | ||||
|         ) { | ||||
|  |  | |||
|  | @ -121,9 +121,9 @@ export default class UploadTraceToOsmUI extends LoginToggle { | |||
|                     ]).SetClass("flex p-2 rounded-xl border-2 subtle-border items-center"), | ||||
|                     new Toggle( | ||||
|                         confirmPanel, | ||||
|                         new SubtleButton(new SvelteUIElement(Upload), t.title).onClick(() => | ||||
|                             clicked.setData(true) | ||||
|                         ), | ||||
|                         new SubtleButton(new SvelteUIElement(Upload), t.title) | ||||
|                             .onClick(() => clicked.setData(true)) | ||||
|                             .SetClass("w-full"), | ||||
|                         clicked | ||||
|                     ), | ||||
|                     uploadFinished | ||||
|  |  | |||
|  | @ -0,0 +1,10 @@ | |||
| <script lang="ts"> | ||||
| 
 | ||||
|   export let properties: Record<string, string> | ||||
| </script> | ||||
| 
 | ||||
| <div> | ||||
|   {JSON.stringify(properties)} | ||||
|   {properties?.id ?? "undefined"} | ||||
|   <a href={properties._backend +"/"+ properties?.id}>OSM</a> | ||||
| </div> | ||||
|  | @ -1,8 +1,19 @@ | |||
| <script lang="ts"> | ||||
|   import { SpecialVisualization } from "../SpecialVisualization"; | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||
|   import FavouriteSummary from "./FavouriteSummary.svelte"; | ||||
| 
 | ||||
|   /** | ||||
|    * A panel showing all your favourites | ||||
|    */ | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let state: SpecialVisualizationState; | ||||
|   let favourites = state.favourites.allFavourites; | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col"> | ||||
|   You marked {$favourites.length} locations as a favourite location. | ||||
|    | ||||
|   This list is only visible to you | ||||
| {#each $favourites as f} | ||||
|   <FavouriteSummary properties={f.properties} /> | ||||
| {/each} | ||||
| </div> | ||||
|  |  | |||
|  | @ -31,14 +31,16 @@ export class ExportAsGpxViz implements SpecialVisualization { | |||
|                 t.downloadFeatureAsGpx.SetClass("font-bold text-lg"), | ||||
|                 t.downloadGpxHelper.SetClass("subtle"), | ||||
|             ]).SetClass("flex flex-col") | ||||
|         ).onClick(() => { | ||||
|             console.log("Exporting as GPX!") | ||||
|             const tags = tagSource.data | ||||
|             const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" | ||||
|             const gpx = GeoOperations.toGpx(<Feature<LineString>>feature, title) | ||||
|             Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", { | ||||
|                 mimetype: "{gpx=application/gpx+xml}", | ||||
|         ) | ||||
|             .SetClass("w-full") | ||||
|             .onClick(() => { | ||||
|                 console.log("Exporting as GPX!") | ||||
|                 const tags = tagSource.data | ||||
|                 const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" | ||||
|                 const gpx = GeoOperations.toGpx(<Feature<LineString>>feature, title) | ||||
|                 Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", { | ||||
|                     mimetype: "{gpx=application/gpx+xml}", | ||||
|                 }) | ||||
|             }) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -534,6 +534,9 @@ export default class SpecialVisualizations { | |||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                 ): BaseUIElement { | ||||
|                     if (!layer.deletion) { | ||||
|                         return undefined | ||||
|                     } | ||||
|                     return new SvelteUIElement(DeleteWizard, { | ||||
|                         tags: tagSource, | ||||
|                         deleteConfig: layer.deletion, | ||||
|  | @ -873,20 +876,22 @@ export default class SpecialVisualizations { | |||
|                             t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"), | ||||
|                             t.downloadGeoJsonHelper.SetClass("subtle"), | ||||
|                         ]).SetClass("flex flex-col") | ||||
|                     ).onClick(() => { | ||||
|                         console.log("Exporting as Geojson") | ||||
|                         const tags = tagSource.data | ||||
|                         const title = | ||||
|                             layer?.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson" | ||||
|                         const data = JSON.stringify(feature, null, "  ") | ||||
|                         Utils.offerContentsAsDownloadableFile( | ||||
|                             data, | ||||
|                             title + "_mapcomplete_export.geojson", | ||||
|                             { | ||||
|                                 mimetype: "application/vnd.geo+json", | ||||
|                             } | ||||
|                         ) | ||||
|                     }) | ||||
|                     ) | ||||
|                         .onClick(() => { | ||||
|                             console.log("Exporting as Geojson") | ||||
|                             const tags = tagSource.data | ||||
|                             const title = | ||||
|                                 layer?.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson" | ||||
|                             const data = JSON.stringify(feature, null, "  ") | ||||
|                             Utils.offerContentsAsDownloadableFile( | ||||
|                                 data, | ||||
|                                 title + "_mapcomplete_export.geojson", | ||||
|                                 { | ||||
|                                     mimetype: "application/vnd.geo+json", | ||||
|                                 } | ||||
|                             ) | ||||
|                         }) | ||||
|                         .SetClass("w-full") | ||||
|                 }, | ||||
|             }, | ||||
|             { | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ | |||
|   import type { MapProperties } from "../Models/MapProperties"; | ||||
|   import Geosearch from "./BigComponents/Geosearch.svelte"; | ||||
|   import Translations from "./i18n/Translations"; | ||||
|   import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import Tr from "./Base/Tr.svelte"; | ||||
|   import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"; | ||||
|   import FloatOver from "./Base/FloatOver.svelte"; | ||||
|  | @ -64,6 +64,8 @@ | |||
|   import Community from "../assets/svg/Community.svelte"; | ||||
|   import Download from "../assets/svg/Download.svelte"; | ||||
|   import Share from "../assets/svg/Share.svelte"; | ||||
|   import FavouriteSummary from "./Favourites/FavouriteSummary.svelte"; | ||||
|   import Favourites from "./Favourites/Favourites.svelte"; | ||||
| 
 | ||||
|   export let state: ThemeViewState; | ||||
|   let layout = state.layout; | ||||
|  | @ -493,22 +495,31 @@ | |||
|       </div> | ||||
| 
 | ||||
|       <div class="flex" slot="title2"> | ||||
|         <HeartIcon class="h-6 w-6" /> | ||||
|         Your favourites | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="flex flex-col" slot="content2"> | ||||
|         <h3>Your favourite locations</h3> | ||||
|         <Favourites {state}/> | ||||
|       </div> | ||||
|       <div class="flex" slot="title3"> | ||||
|         <Community class="w-6 h-6"/> | ||||
|         <Tr t={Translations.t.communityIndex.title} /> | ||||
|       </div> | ||||
|       <div class="m-2" slot="content2"> | ||||
|       <div class="m-2" slot="content3"> | ||||
|         <CommunityIndexView location={state.mapProperties.location} /> | ||||
|       </div> | ||||
|       <div class="flex" slot="title3"> | ||||
|       <div class="flex" slot="title4"> | ||||
|         <EyeIcon class="w-6" /> | ||||
|         <Tr t={Translations.t.privacy.title} /> | ||||
|       </div> | ||||
|       <div class="m-2" slot="content3"> | ||||
|       <div class="m-2" slot="content4"> | ||||
|         <ToSvelte construct={() => new PrivacyPolicy()} /> | ||||
|       </div> | ||||
| 
 | ||||
|       <Tr slot="title4" t={Translations.t.advanced.title} /> | ||||
|       <div class="m-2 flex flex-col" slot="content4"> | ||||
|       <Tr slot="title5" t={Translations.t.advanced.title} /> | ||||
|       <div class="m-2 flex flex-col" slot="content5"> | ||||
|         <If condition={featureSwitches.featureSwitchEnableLogin}> | ||||
|           <OpenIdEditor mapProperties={state.mapProperties} /> | ||||
|           <OpenJosm {state}/> | ||||
|  |  | |||
|  | @ -179,7 +179,21 @@ describe("PrepareTheme", () => { | |||
|             id: "layer-example", | ||||
|             name: null, | ||||
|             minzoom: 18, | ||||
|             pointRendering: [{ location: ["point"], label: "xyz" }], | ||||
|             pointRendering: [ | ||||
|                 { | ||||
|                     location: ["point"], | ||||
|                     label: "xyz", | ||||
|                     iconBadges: [ | ||||
|                         { | ||||
|                             if: "_favourite=yes", | ||||
|                             then: { | ||||
|                                 id: "circlewhiteheartred", | ||||
|                                 render: "circle:white;heart:red", | ||||
|                             }, | ||||
|                         }, | ||||
|                     ], | ||||
|                 }, | ||||
|             ], | ||||
|             lineRendering: [{ width: 1 }], | ||||
|             titleIcons: [], | ||||
|         }) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue