forked from MapComplete/MapComplete
		
	Finish importer, add applicable import layers to every theme by default
This commit is contained in:
		
							parent
							
								
									3402ac0954
								
							
						
					
					
						commit
						ca1490902c
					
				
					 41 changed files with 1559 additions and 898 deletions
				
			
		|  | @ -10,11 +10,12 @@ import {UIEventSource} from "./UIEventSource"; | |||
| import {LocalStorageSource} from "./Web/LocalStorageSource"; | ||||
| import LZString from "lz-string"; | ||||
| import * as personal from "../assets/themes/personal/personal.json"; | ||||
| import {FixLegacyTheme, PrepareTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; | ||||
| import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; | ||||
| import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | ||||
| import SharedTagRenderings from "../Customizations/SharedTagRenderings"; | ||||
| import * as known_layers from "../assets/generated/known_layers.json" | ||||
| import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; | ||||
| import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme"; | ||||
| 
 | ||||
| export default class DetermineLayout { | ||||
| 
 | ||||
|  |  | |||
|  | @ -29,11 +29,11 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti | |||
|         }, | ||||
|         tileIndex, | ||||
|         upstream: FeatureSourceForLayer, | ||||
|         metataggingUpdated: UIEventSource<any> | ||||
|         metataggingUpdated?: UIEventSource<any> | ||||
|     ) { | ||||
|         this.name = "FilteringFeatureSource(" + upstream.name + ")" | ||||
|         this.tileIndex = tileIndex | ||||
|         this.bbox = BBox.fromTileIndex(tileIndex) | ||||
|         this.bbox = tileIndex === undefined ? undefined : BBox.fromTileIndex(tileIndex) | ||||
|         this.upstream = upstream | ||||
|         this.state = state | ||||
| 
 | ||||
|  | @ -55,7 +55,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti | |||
|             } | ||||
|         }) | ||||
|          | ||||
|         metataggingUpdated.addCallback(_ => { | ||||
|         metataggingUpdated?.addCallback(_ => { | ||||
|             self._is_dirty.setData(true) | ||||
|         }) | ||||
| 
 | ||||
|  | @ -63,6 +63,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti | |||
|     } | ||||
| 
 | ||||
|     private update() { | ||||
|         console.log("FIltering", this.upstream.name) | ||||
|         const self = this; | ||||
|         const layer = this.upstream.layer; | ||||
|         const features: { feature: any; freshness: Date }[] = (this.upstream.features.data ?? []); | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | |||
|     private readonly featureIdBlacklist?: UIEventSource<Set<string>> | ||||
| 
 | ||||
|     public constructor(flayer: FilteredLayer, | ||||
|                        zxy?: [number, number, number], | ||||
|                        zxy?: [number, number, number] | BBox, | ||||
|                        options?: { | ||||
|                            featureIdBlacklist?: UIEventSource<Set<string>> | ||||
|                        }) { | ||||
|  | @ -41,23 +41,32 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | |||
|         this.featureIdBlacklist = options?.featureIdBlacklist | ||||
|         let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); | ||||
|         if (zxy !== undefined) { | ||||
|             const [z, x, y] = zxy; | ||||
|             let tile_bbox = BBox.fromTile(z, x, y) | ||||
|             let tile_bbox: BBox; | ||||
|             if (zxy instanceof BBox) { | ||||
|                 tile_bbox = zxy; | ||||
|             } else { | ||||
|                 const [z, x, y] = zxy; | ||||
|                 tile_bbox = BBox.fromTile(z, x, y); | ||||
| 
 | ||||
|                 this.tileIndex = Tiles.tile_index(z, x, y) | ||||
|                 this.bbox = BBox.fromTile(z, x, y) | ||||
|                 url = url | ||||
|                     .replace('{z}', "" + z) | ||||
|                     .replace('{x}', "" + x) | ||||
|                     .replace('{y}', "" + y) | ||||
|             } | ||||
|             let bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox | ||||
|             if (this.layer.layerDef.source.mercatorCrs) { | ||||
|                 bounds = tile_bbox.toMercator() | ||||
|             } | ||||
| 
 | ||||
|             url = url | ||||
|                 .replace('{z}', "" + z) | ||||
|                 .replace('{x}', "" + x) | ||||
|                 .replace('{y}', "" + y) | ||||
|                 .replace('{y_min}', "" + bounds.minLat) | ||||
|                 .replace('{y_max}', "" + bounds.maxLat) | ||||
|                 .replace('{x_min}', "" + bounds.minLon) | ||||
|                 .replace('{x_max}', "" + bounds.maxLon) | ||||
| 
 | ||||
|             this.tileIndex = Tiles.tile_index(z, x, y) | ||||
|             this.bbox = BBox.fromTile(z, x, y) | ||||
| 
 | ||||
|         } else { | ||||
|             this.tileIndex = Tiles.tile_index(0, 0, 0) | ||||
|             this.bbox = BBox.global; | ||||
|  | @ -83,7 +92,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | |||
|                 if (self.layer.layerDef.source.mercatorCrs) { | ||||
|                     json = GeoOperations.GeoJsonToWGS84(json) | ||||
|                 } | ||||
|                  | ||||
| 
 | ||||
|                 const time = new Date(); | ||||
|                 const newFeatures: { feature: any, freshness: Date } [] = [] | ||||
|                 let i = 0; | ||||
|  |  | |||
|  | @ -683,6 +683,8 @@ export class GeoOperations { | |||
|         throw "CalculateIntersection fallthrough: can not calculate an intersection between features" | ||||
| 
 | ||||
|     } | ||||
|      | ||||
|      | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -18,13 +18,13 @@ export class ChangesetHandler { | |||
|     private readonly allElements: ElementStorage; | ||||
|     private osmConnection: OsmConnection; | ||||
|     private readonly changes: Changes; | ||||
|     private readonly _dryRun: boolean; | ||||
|     private readonly _dryRun: UIEventSource<boolean>; | ||||
|     private readonly userDetails: UIEventSource<UserDetails>; | ||||
|     private readonly auth: any; | ||||
|     private readonly backend: string; | ||||
| 
 | ||||
|     constructor(layoutName: string, | ||||
|                 dryRun: boolean, | ||||
|                 dryRun: UIEventSource<boolean>, | ||||
|                 osmConnection: OsmConnection, | ||||
|                 allElements: ElementStorage, | ||||
|                 changes: Changes, | ||||
|  | @ -67,7 +67,7 @@ export class ChangesetHandler { | |||
|             this.userDetails.data.csCount = 1; | ||||
|             this.userDetails.ping(); | ||||
|         } | ||||
|         if (this._dryRun) { | ||||
|         if (this._dryRun.data) { | ||||
|             const changesetXML = generateChangeXML(123456); | ||||
|             console.log("Metatags are", extraMetaTags) | ||||
|             console.log(changesetXML); | ||||
|  |  | |||
|  | @ -19,7 +19,6 @@ export default class UserDetails { | |||
|     public img: string; | ||||
|     public unreadMessages = 0; | ||||
|     public totalMessages = 0; | ||||
|     public dryRun: boolean; | ||||
|     home: { lon: number; lat: number }; | ||||
|     public backend: string; | ||||
| 
 | ||||
|  | @ -47,7 +46,6 @@ export class OsmConnection { | |||
|     public auth; | ||||
|     public userDetails: UIEventSource<UserDetails>; | ||||
|     public isLoggedIn: UIEventSource<boolean> | ||||
|     _dryRun: boolean; | ||||
|     public preferencesHandler: OsmPreferences; | ||||
|     public changesetHandler: ChangesetHandler; | ||||
|     public readonly _oauth_config: { | ||||
|  | @ -55,6 +53,7 @@ export class OsmConnection { | |||
|         oauth_secret: string, | ||||
|         url: string | ||||
|     }; | ||||
|     private readonly _dryRun: UIEventSource<boolean>; | ||||
|     private fakeUser: boolean; | ||||
|     private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []; | ||||
|     private readonly _iframeMode: Boolean | boolean; | ||||
|  | @ -62,7 +61,7 @@ export class OsmConnection { | |||
|     private isChecking = false; | ||||
| 
 | ||||
|     constructor(options: { | ||||
|                     dryRun?: false | boolean, | ||||
|                     dryRun?: UIEventSource<boolean>, | ||||
|                     fakeUser?: false | boolean, | ||||
|                     allElements: ElementStorage, | ||||
|                     changes: Changes, | ||||
|  | @ -82,7 +81,6 @@ export class OsmConnection { | |||
|         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; | ||||
| 
 | ||||
|         this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails"); | ||||
|         this.userDetails.data.dryRun = (options.dryRun ?? false) || (options.fakeUser ?? false); | ||||
|         if (options.fakeUser) { | ||||
|             const ud = this.userDetails.data; | ||||
|             ud.csCount = 5678 | ||||
|  | @ -99,13 +97,13 @@ export class OsmConnection { | |||
|                 self.AttemptLogin() | ||||
|             } | ||||
|         }); | ||||
|         this._dryRun = options.dryRun; | ||||
|         this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false); | ||||
| 
 | ||||
|         this.updateAuthObject(); | ||||
| 
 | ||||
|         this.preferencesHandler = new OsmPreferences(this.auth, this); | ||||
| 
 | ||||
|         this.changesetHandler = new ChangesetHandler(options.layoutName, options.dryRun, this, options.allElements, options.changes, this.auth); | ||||
|         this.changesetHandler = new ChangesetHandler(options.layoutName, this._dryRun, this, options.allElements, options.changes, this.auth); | ||||
|         if (options.oauth_token?.data !== undefined) { | ||||
|             console.log(options.oauth_token.data) | ||||
|             const self = this; | ||||
|  | @ -223,7 +221,7 @@ export class OsmConnection { | |||
|         if ((text ?? "") !== "") { | ||||
|             textSuffix = "?text=" + encodeURIComponent(text) | ||||
|         } | ||||
|         if (this._dryRun) { | ||||
|         if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) | ||||
|             return new Promise((ok, error) => { | ||||
|                 ok() | ||||
|  | @ -246,7 +244,7 @@ export class OsmConnection { | |||
|     } | ||||
| 
 | ||||
|     public reopenNote(id: number | string, text?: string): Promise<any> { | ||||
|         if (this._dryRun) { | ||||
|         if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) | ||||
|             return new Promise((ok, error) => { | ||||
|                 ok() | ||||
|  | @ -273,10 +271,10 @@ export class OsmConnection { | |||
|     } | ||||
| 
 | ||||
|     public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { | ||||
|         if (this._dryRun) { | ||||
|         if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually opening note with text ", text) | ||||
|             return new Promise((ok, error) => { | ||||
|                 ok() | ||||
|             return new Promise<{ id: number }>((ok, error) => { | ||||
|                 window.setTimeout(() => ok({id: Math.floor(Math.random() * 1000)}), Math.random() * 5000) | ||||
|             }); | ||||
|         } | ||||
|         const auth = this.auth; | ||||
|  | @ -285,15 +283,18 @@ export class OsmConnection { | |||
|             auth.xhr({ | ||||
|                 method: 'POST', | ||||
|                 path: `/api/0.6/notes.json`, | ||||
|                 options: {header: | ||||
|                         {'Content-Type': 'application/json'}}, | ||||
|                 options: { | ||||
|                     header: | ||||
|                         {'Content-Type': 'application/json'} | ||||
|                 }, | ||||
|                 content: JSON.stringify(content) | ||||
| 
 | ||||
|             }, function (err, response) { | ||||
|                 if (err !== null) { | ||||
|                     error(err) | ||||
|                 } else { | ||||
|                     const id = Number(response.children[0].children[0].children.item("id").innerHTML) | ||||
| 
 | ||||
|                     const id = response.properties.id | ||||
|                     console.log("OPENED NOTE", id) | ||||
|                     ok({id}) | ||||
|                 } | ||||
|  | @ -304,7 +305,7 @@ export class OsmConnection { | |||
|     } | ||||
| 
 | ||||
|     public addCommentToNode(id: number | string, text: string): Promise<any> { | ||||
|         if (this._dryRun) { | ||||
|         if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually adding comment ", text, "to  note ", id) | ||||
|             return new Promise((ok, error) => { | ||||
|                 ok() | ||||
|  | @ -317,7 +318,7 @@ export class OsmConnection { | |||
|         return new Promise((ok, error) => { | ||||
|             this.auth.xhr({ | ||||
|                 method: 'POST', | ||||
|                  | ||||
| 
 | ||||
|                 path: `/api/0.6/notes.json/${id}/comment?text=${encodeURIComponent(text)}` | ||||
|             }, function (err, response) { | ||||
|                 if (err !== null) { | ||||
|  |  | |||
|  | @ -43,7 +43,7 @@ export default class UserRelatedState extends ElementsState { | |||
| 
 | ||||
|         this.osmConnection = new OsmConnection({ | ||||
|             changes: this.changes, | ||||
|             dryRun: this.featureSwitchIsTesting.data, | ||||
|             dryRun: this.featureSwitchIsTesting, | ||||
|             fakeUser: this.featureSwitchFakeUser.data, | ||||
|             allElements: this.allElements, | ||||
|             oauth_token: QueryParameters.GetQueryParameter( | ||||
|  |  | |||
|  | @ -10,6 +10,13 @@ export class RegexTag extends TagsFilter { | |||
|     constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) { | ||||
|         super(); | ||||
|         this.key = key; | ||||
|         if (typeof value === "string") { | ||||
|             if (value.indexOf("^") < 0 && value.indexOf("$") < 0) { | ||||
|                 value = "^" + value + "$" | ||||
|             } | ||||
|             value = new RegExp(value) | ||||
|         } | ||||
| 
 | ||||
|         this.value = value; | ||||
|         this.invert = invert; | ||||
|         this.matchesEmpty = RegexTag.doesMatch("", this.value); | ||||
|  | @ -109,7 +116,7 @@ export class RegexTag extends TagsFilter { | |||
|         console.error("Cannot export regex tag to asChange; ", this.key, this.value) | ||||
|         return [] | ||||
|     } | ||||
|      | ||||
| 
 | ||||
|     AsJson() { | ||||
|         return this.asHumanString() | ||||
|     } | ||||
|  |  | |||
|  | @ -192,16 +192,16 @@ export class TagUtils { | |||
|                     } | ||||
| 
 | ||||
|                     const f = (value: string | undefined) => { | ||||
|                         if(value === undefined){ | ||||
|                         if (value === undefined) { | ||||
|                             return false; | ||||
|                         } | ||||
|                         let b = Number(value?.trim() ) | ||||
|                         let b = Number(value?.trim()) | ||||
|                         if (isNaN(b)) { | ||||
|                             if(value.endsWith(" UTC")) { | ||||
|                             if (value.endsWith(" UTC")) { | ||||
|                                 value = value.replace(" UTC", "+00") | ||||
|                             } | ||||
|                             b = new Date(value).getTime() | ||||
|                             if(isNaN(b)){ | ||||
|                             if (isNaN(b)) { | ||||
|                                 return false | ||||
|                             } | ||||
|                         } | ||||
|  | @ -218,7 +218,7 @@ export class TagUtils { | |||
|                 } | ||||
|                 return new RegexTag( | ||||
|                     split[0], | ||||
|                     new RegExp("^" + split[1] + "$"), | ||||
|                     split[1], | ||||
|                     true | ||||
|                 ); | ||||
|             } | ||||
|  | @ -228,8 +228,8 @@ export class TagUtils { | |||
|                     split[1] = "..*" | ||||
|                 } | ||||
|                 return new RegexTag( | ||||
|                     new RegExp("^" + split[0] + "$"), | ||||
|                     new RegExp("^" + split[1] + "$") | ||||
|                     split[0], | ||||
|                     split[1] | ||||
|                 ); | ||||
|             } | ||||
|             if (tag.indexOf("!:=") >= 0) { | ||||
|  | @ -248,7 +248,7 @@ export class TagUtils { | |||
|                 } | ||||
|                 return new RegexTag( | ||||
|                     split[0], | ||||
|                     new RegExp("^" + split[1] + "$"), | ||||
|                    new RegExp("^" + split[1] + "$"), | ||||
|                     true | ||||
|                 ); | ||||
|             } | ||||
|  | @ -259,7 +259,7 @@ export class TagUtils { | |||
|                 } | ||||
|                 return new RegexTag( | ||||
|                     split[0], | ||||
|                     new RegExp("^" + split[1] + "$"), | ||||
|                     split[1], | ||||
|                     true | ||||
|                 ); | ||||
|             } | ||||
|  | @ -273,7 +273,7 @@ export class TagUtils { | |||
|                 } | ||||
|                 return new RegexTag( | ||||
|                     split[0], | ||||
|                     new RegExp("^" + split[1] + "$") | ||||
|                     split[1] | ||||
|                 ); | ||||
|             } | ||||
|             if (tag.indexOf("=") >= 0) { | ||||
|  |  | |||
							
								
								
									
										171
									
								
								Models/ThemeConfig/Conversion/Conversion.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								Models/ThemeConfig/Conversion/Conversion.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,171 @@ | |||
| import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"; | ||||
| import {LayerConfigJson} from "../Json/LayerConfigJson"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| 
 | ||||
| export interface DesugaringContext { | ||||
|     tagRenderings: Map<string, TagRenderingConfigJson> | ||||
|     sharedLayers: Map<string, LayerConfigJson> | ||||
| } | ||||
| 
 | ||||
| export abstract class Conversion<TIn, TOut> { | ||||
|     public readonly modifiedAttributes: string[]; | ||||
|     protected readonly doc: string; | ||||
| 
 | ||||
|     constructor(doc: string, modifiedAttributes: string[] = []) { | ||||
|         this.modifiedAttributes = modifiedAttributes; | ||||
|         this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", "); | ||||
|     } | ||||
| 
 | ||||
|     public static strict<T>(fixed: { errors: string[], warnings: string[], result?: T }): T { | ||||
|         if (fixed?.errors?.length > 0) { | ||||
|             throw fixed.errors.join("\n"); | ||||
|         } | ||||
|         fixed.warnings?.forEach(w => console.warn(w)) | ||||
|         return fixed.result; | ||||
|     } | ||||
| 
 | ||||
|     public convertStrict(state: DesugaringContext, json: TIn, context: string): TOut { | ||||
|         const fixed = this.convert(state, json, context) | ||||
|         return DesugaringStep.strict(fixed) | ||||
|     } | ||||
| 
 | ||||
|     abstract convert(state: DesugaringContext, json: TIn, context: string): { result: TOut, errors: string[], warnings: string[] } | ||||
| 
 | ||||
|     public convertAll(state: DesugaringContext, jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[] } { | ||||
|         const result = [] | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
|         for (let i = 0; i < jsons.length; i++) { | ||||
|             const json = jsons[i]; | ||||
|             const r = this.convert(state, json, context + "[" + i + "]") | ||||
|             result.push(r.result) | ||||
|             errors.push(...r.errors) | ||||
|             warnings.push(...r.warnings) | ||||
|         } | ||||
|         return { | ||||
|             result, | ||||
|             errors, | ||||
|             warnings | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export abstract class DesugaringStep<T> extends Conversion<T, T> { | ||||
| } | ||||
| 
 | ||||
| export class OnEvery<X, T> extends DesugaringStep<T> { | ||||
|     private readonly key: string; | ||||
|     private readonly step: DesugaringStep<X>; | ||||
| 
 | ||||
|     constructor(key: string, step: DesugaringStep<X>) { | ||||
|         super("Applies " + step.constructor.name + " onto every object of the list `key`", [key]); | ||||
|         this.step = step; | ||||
|         this.key = key; | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } { | ||||
|         json = {...json} | ||||
|         const step = this.step | ||||
|         const key = this.key; | ||||
|         const r = step.convertAll(state, (<X[]>json[key]), context + "." + key) | ||||
|         json[key] = r.result | ||||
|         return { | ||||
|             result: json, | ||||
|             errors: r.errors, | ||||
|             warnings: r.warnings | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class OnEveryConcat<X, T> extends DesugaringStep<T> { | ||||
|     private readonly key: string; | ||||
|     private readonly step: Conversion<X, X[]>; | ||||
| 
 | ||||
|     constructor(key: string, step: Conversion<X, X[]>) { | ||||
|         super(`Applies ${step.constructor.name} onto every object of the list \`${key}\`. The results are concatenated and used as new list`, [key]); | ||||
|         this.step = step; | ||||
|         this.key = key; | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } { | ||||
|         json = {...json} | ||||
|         const step = this.step | ||||
|         const key = this.key; | ||||
|         const values = json[key] | ||||
|         if (values === undefined) { | ||||
|             // Move on - nothing to see here!
 | ||||
|             return { | ||||
|                 result: json, | ||||
|                 errors: [], | ||||
|                 warnings: [] | ||||
|             } | ||||
|         } | ||||
|         const r = step.convertAll(state, (<X[]>values), context + "." + key) | ||||
|         const vals: X[][] = r.result | ||||
|         json[key] = [].concat(...vals) | ||||
|         return { | ||||
|             result: json, | ||||
|             errors: r.errors, | ||||
|             warnings: r.warnings | ||||
|         }; | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class Fuse<T> extends DesugaringStep<T> { | ||||
|     private readonly steps: DesugaringStep<T>[]; | ||||
| 
 | ||||
|     constructor(doc: string, ...steps: DesugaringStep<T>[]) { | ||||
|         super((doc ?? "") + "This fused pipeline of the following steps: " + steps.map(s => s.constructor.name).join(", "), | ||||
|             Utils.Dedup([].concat(...steps.map(step => step.modifiedAttributes))) | ||||
|         ); | ||||
|         this.steps = steps; | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } { | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
|         for (let i = 0; i < this.steps.length; i++) { | ||||
|             const step = this.steps[i]; | ||||
|             let r = step.convert(state, json, context + "(fusion " + this.constructor.name + "." + i + ")") | ||||
|             errors.push(...r.errors) | ||||
|             warnings.push(...r.warnings) | ||||
|             json = r.result | ||||
|             if (errors.length > 0) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         return { | ||||
|             result: json, | ||||
|             errors, | ||||
|             warnings | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class SetDefault<T> extends DesugaringStep<T> { | ||||
|     private readonly value: any; | ||||
|     private readonly key: string; | ||||
|     private readonly _overrideEmptyString: boolean; | ||||
| 
 | ||||
|     constructor(key: string, value: any, overrideEmptyString = false) { | ||||
|         super("Sets " + key + " to a default value if undefined"); | ||||
|         this.key = key; | ||||
|         this.value = value; | ||||
|         this._overrideEmptyString = overrideEmptyString; | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } { | ||||
|         if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) { | ||||
|             json = {...json} | ||||
|             json[this.key] = this.value | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             errors: [], warnings: [], | ||||
|             result: json | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | @ -1,29 +1,69 @@ | |||
| import {Conversion, DesugaringContext} from "./LegacyJsonConvert"; | ||||
| import {Conversion, DesugaringContext} from "./Conversion"; | ||||
| import LayerConfig from "../LayerConfig"; | ||||
| import {LayerConfigJson} from "../Json/LayerConfigJson"; | ||||
| import Translations from "../../../UI/i18n/Translations"; | ||||
| import {TagsFilter} from "../../../Logic/Tags/TagsFilter"; | ||||
| import {And} from "../../../Logic/Tags/And"; | ||||
| import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"; | ||||
| 
 | ||||
| export default class CreateNoteImportLayer extends Conversion<LayerConfig, LayerConfigJson> { | ||||
| export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, LayerConfigJson> { | ||||
|     /** | ||||
|      * A closed note is included if it is less then 'n'-days closed | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _includeClosedNotesDays: number; | ||||
| 
 | ||||
|     constructor() { | ||||
|     constructor(includeClosedNotesDays= 0) { | ||||
|         super([ | ||||
|             "Advanced conversion which deducts a layer showing all notes that are 'importable' (i.e. a note that contains a link to some MapComplete theme, with hash '#import').", | ||||
|             "The import buttons and matches will be based on the presets of the given theme", | ||||
|         ].join("\n\n"), []) | ||||
|         this._includeClosedNotesDays = includeClosedNotesDays; | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, layer: LayerConfig, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } { | ||||
|     convert(state: DesugaringContext, layerJson: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } { | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
|         const t = Translations.t.importLayer; | ||||
|          | ||||
|         const possibleTags: TagsFilter[] = layer.presets.map(p => new And(p.tags)) | ||||
|         /** | ||||
|          * The note itself will contain `tags=k=v;k=v;k=v;...
 | ||||
|          * This must be matched with a regex. | ||||
|          * This is a simple JSON-object as how it'll be put into the layerConfigJson directly | ||||
|          */ | ||||
|         const isShownIfAny : any[] = [] | ||||
|         const layer = new LayerConfig(layerJson, "while constructing a note-import layer") | ||||
|         for (const preset of layer.presets) { | ||||
|             const mustMatchAll = [] | ||||
|             for (const tag of preset.tags) { | ||||
|                 const key = tag.key | ||||
|                 const value = tag.value | ||||
|                 const condition = "_tags~(^|.*;)"+key+"\="+value+"($|;.*)" | ||||
|                 mustMatchAll.push(condition) | ||||
|             } | ||||
|             isShownIfAny.push({and:mustMatchAll}) | ||||
|         } | ||||
|          | ||||
|         const pointRenderings = (layerJson.mapRendering??[]).filter(r => r!== null && r["location"] !== undefined); | ||||
|         const firstRender =  <PointRenderingConfigJson>(pointRenderings [0]) | ||||
|         const icon = firstRender.icon | ||||
|         const iconBadges = [] | ||||
|         if(icon !== undefined){ | ||||
|             iconBadges.push({ | ||||
|                 if: {and:[]}, | ||||
|                 then:icon | ||||
|             }) | ||||
|         } | ||||
|          | ||||
|         const importButton = {} | ||||
|         { | ||||
|         const translations = t.importButton.Subs({layerId: layer.id, title: layer.presets[0].title}).translations | ||||
|             for (const key in translations) { | ||||
|                 importButton[key] = "{"+translations[key]+"}" | ||||
|             }     | ||||
|         } | ||||
|          | ||||
|         const result : LayerConfigJson = { | ||||
|             "id": "note_import_"+layer.id, | ||||
|             "name": t.layerName.Subs({title: layer.title.render}).translations, | ||||
|             // By disabling the name, the import-layers won't pollute the filter view "name": t.layerName.Subs({title: layer.title.render}).translations,
 | ||||
|             "description": t.description.Subs({title: layer.title.render}).translations, | ||||
|             "source": { | ||||
|                 "osmTags": { | ||||
|  | @ -31,27 +71,32 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer | |||
|                         "id~*" | ||||
|                     ] | ||||
|                 }, | ||||
|                 "geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=0&bbox={x_min},{y_min},{x_max},{y_max}", | ||||
|                 "geoJsonZoomLevel": 12, | ||||
|                 "geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed="+this._includeClosedNotesDays+"&bbox={x_min},{y_min},{x_max},{y_max}", | ||||
|                 "geoJsonZoomLevel": 10, | ||||
|                 "maxCacheAge": 0 | ||||
|             }, | ||||
|             "minzoom": 10, | ||||
|             "minzoom": 12, | ||||
|             "title": { | ||||
|                 "render": t.popupTitle.Subs({title: layer.presets[0].title}).translations | ||||
|             }, | ||||
|             "calculatedTags": [ | ||||
|                 "_first_comment:=feat.get('comments')[0].text.toLowerCase()", | ||||
|                 "_trigger_index:=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\).*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()", | ||||
|                 "_intro:=(() => {const lines = feat.properties['_first_comment'].split('\\n'); lines.splice(feat.get('_trigger_index')-1, lines.length); return lines.map(l => l == '' ? '<br/>' : l).join('');})()", | ||||
|                 "_tags:=(() => {let lines = feat.properties['_first_comment'].split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()" | ||||
|                 "_first_comment=feat.get('comments')[0].text.toLowerCase()", | ||||
|                 "_trigger_index=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\).*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()", | ||||
|                 "_comments_count=feat.get('comments').length", | ||||
|                 "_intro=(() => {const lines = feat.properties['_first_comment'].split('\\n'); lines.splice(feat.get('_trigger_index')-1, lines.length); return lines.filter(l => l !== '').join('<br/>');})()", | ||||
|                 "_tags=(() => {let lines = feat.properties['_first_comment'].split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()" | ||||
|             ], | ||||
|             "isShown": { | ||||
|                 "render": "no", | ||||
|                 "mappings": [ | ||||
|                     { | ||||
|                         "if": "comments!~.*https://mapcomplete.osm.be.*", | ||||
|                         "then":"no" | ||||
|                     }, | ||||
|                     { | ||||
|                         "if": {and:  | ||||
|                                 ["_trigger_index~*", | ||||
|                                     {or: possibleTags.map(tf => tf.AsJson())} | ||||
|                                     {or: isShownIfAny} | ||||
|                                 ]}, | ||||
|                         "then": "yes" | ||||
|                     } | ||||
|  | @ -63,25 +108,34 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer | |||
|                 } | ||||
|             ], | ||||
|             "tagRenderings": [ | ||||
|                 { | ||||
|                     "id": "conversation", | ||||
|                     "render": "{visualize_note_comments(comments,1)}" | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": "Intro", | ||||
|                     "render": "{_intro}" | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": "conversation", | ||||
|                     "render": "{visualize_note_comments(comments,1)}", | ||||
|                     condition: "_comments_count>1" | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": "import", | ||||
|                     "render": "{import_button(public_bookcase, _tags, There might be a public bookcase here,./assets/svg/addSmall.svg,,,id)}" | ||||
|                     "render": importButton, | ||||
|                     condition: "closed_at=" | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": "close_note_", | ||||
|                     "render": "{close_note(Does not exist<br/>, ./assets/svg/close.svg, id, This feature does not exist)}" | ||||
|                     "render": "{close_note(Does not exist<br/>, ./assets/svg/close.svg, id, This feature does not exist)}", | ||||
|                     condition: "closed_at=" | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": "close_note_mapped", | ||||
|                     "render": "{close_note(Already mapped, ./assets/svg/checkmark.svg, id, Already mapped)}" | ||||
|                     "render": "{close_note(Already mapped, ./assets/svg/checkmark.svg, id, Already mapped)}", | ||||
|                     condition: "closed_at=" | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": "handled", | ||||
|                     "render": t.importHandled.translations, | ||||
|                     condition: "closed_at~*" | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": "comment", | ||||
|  | @ -90,6 +144,10 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer | |||
|                 { | ||||
|                     "id": "add_image", | ||||
|                     "render": "{add_image_to_note()}" | ||||
|                 }, | ||||
|                 { | ||||
|                     id:"alltags", | ||||
|                     render:"{all_tags()}" | ||||
|                 } | ||||
|             ], | ||||
|             "mapRendering": [ | ||||
|  | @ -99,9 +157,14 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer | |||
|                         "centroid" | ||||
|                     ], | ||||
|                     "icon": { | ||||
|                         "render": "teardrop:#3333cc" | ||||
|                         "render": "circle:white;help:black", | ||||
|                         mappings:[{ | ||||
|                             if: {or:["closed_at~*","_imported=yes"]}, | ||||
|                             then:"circle:white;checkmark:black" | ||||
|                         }] | ||||
|                     }, | ||||
|                     "iconSize": "40,40,bottom" | ||||
|                     iconBadges, | ||||
|                     "iconSize": "40,40,center" | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|  |  | |||
|  | @ -1,439 +1,12 @@ | |||
| import {LayoutConfigJson} from "../Json/LayoutConfigJson"; | ||||
| import DependencyCalculator from "../DependencyCalculator"; | ||||
| import LayerConfig from "../LayerConfig"; | ||||
| import {Translation} from "../../../UI/i18n/Translation"; | ||||
| import LayoutConfig from "../LayoutConfig"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"; | ||||
| import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"; | ||||
| import {LayerConfigJson} from "../Json/LayerConfigJson"; | ||||
| import Constants from "../../Constants"; | ||||
| import {AllKnownLayouts} from "../../../Customizations/AllKnownLayouts"; | ||||
| import {SubstitutedTranslation} from "../../../UI/SubstitutedTranslation"; | ||||
| 
 | ||||
| export interface DesugaringContext { | ||||
|     tagRenderings: Map<string, TagRenderingConfigJson> | ||||
|     sharedLayers: Map<string, LayerConfigJson> | ||||
| } | ||||
| 
 | ||||
| export abstract class Conversion<TIn, TOut> { | ||||
|     public readonly modifiedAttributes: string[]; | ||||
|     protected readonly doc: string; | ||||
| 
 | ||||
|     constructor(doc: string, modifiedAttributes: string[] = []) { | ||||
|         this.modifiedAttributes = modifiedAttributes; | ||||
|         this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", "); | ||||
|     } | ||||
| 
 | ||||
|     public static strict<T>(fixed: { errors: string[], warnings: string[], result?: T }): T { | ||||
|         if (fixed?.errors?.length > 0) { | ||||
|             throw fixed.errors.join("\n"); | ||||
|         } | ||||
|         fixed.warnings?.forEach(w => console.warn(w)) | ||||
|         return fixed.result; | ||||
|     } | ||||
| 
 | ||||
|     public convertStrict(state: DesugaringContext, json: TIn, context: string): TOut { | ||||
|         const fixed = this.convert(state, json, context) | ||||
|         return DesugaringStep.strict(fixed) | ||||
|     } | ||||
| 
 | ||||
|     abstract convert(state: DesugaringContext, json: TIn, context: string): { result: TOut, errors: string[], warnings: string[] } | ||||
| 
 | ||||
|     public convertAll(state: DesugaringContext, jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[] } { | ||||
|         const result = [] | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
|         for (let i = 0; i < jsons.length; i++) { | ||||
|             const json = jsons[i]; | ||||
|             const r = this.convert(state, json, context + "[" + i + "]") | ||||
|             result.push(r.result) | ||||
|             errors.push(...r.errors) | ||||
|             warnings.push(...r.warnings) | ||||
|         } | ||||
|         return { | ||||
|             result, | ||||
|             errors, | ||||
|             warnings | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export abstract class DesugaringStep<T> extends Conversion<T, T> { | ||||
| } | ||||
| 
 | ||||
| class OnEvery<X, T> extends DesugaringStep<T> { | ||||
|     private readonly key: string; | ||||
|     private readonly step: DesugaringStep<X>; | ||||
| 
 | ||||
|     constructor(key: string, step: DesugaringStep<X>) { | ||||
|         super("Applies " + step.constructor.name + " onto every object of the list `key`", [key]); | ||||
|         this.step = step; | ||||
|         this.key = key; | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } { | ||||
|         json = {...json} | ||||
|         const step = this.step | ||||
|         const key = this.key; | ||||
|         const r = step.convertAll(state, (<X[]>json[key]), context + "." + key) | ||||
|         json[key] = r.result | ||||
|         return { | ||||
|             result: json, | ||||
|             errors: r.errors, | ||||
|             warnings: r.warnings | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class OnEveryConcat<X, T> extends DesugaringStep<T> { | ||||
|     private readonly key: string; | ||||
|     private readonly step: Conversion<X, X[]>; | ||||
| 
 | ||||
|     constructor(key: string, step: Conversion<X, X[]>) { | ||||
|         super(`Applies ${step.constructor.name} onto every object of the list \`${key}\``, [key]); | ||||
|         this.step = step; | ||||
|         this.key = key; | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } { | ||||
|         json = {...json} | ||||
|         const step = this.step | ||||
|         const key = this.key; | ||||
|         const values = json[key] | ||||
|         if (values === undefined) { | ||||
|             // Move on - nothing to see here!
 | ||||
|             return { | ||||
|                 result: json, | ||||
|                 errors: [], | ||||
|                 warnings: [] | ||||
|             } | ||||
|         } | ||||
|         const r = step.convertAll(state, (<X[]>values), context + "." + key) | ||||
|         const vals: X[][] = r.result | ||||
|         json[key] = [].concat(...vals) | ||||
|         return { | ||||
|             result: json, | ||||
|             errors: r.errors, | ||||
|             warnings: r.warnings | ||||
|         }; | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class Fuse<T> extends DesugaringStep<T> { | ||||
|     private readonly steps: DesugaringStep<T>[]; | ||||
| 
 | ||||
|     constructor(doc: string, ...steps: DesugaringStep<T>[]) { | ||||
|         super((doc ?? "") + "This fused pipeline of the following steps: " + steps.map(s => s.constructor.name).join(", "), | ||||
|             Utils.Dedup([].concat(...steps.map(step => step.modifiedAttributes))) | ||||
|         ); | ||||
|         this.steps = steps; | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } { | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
|         for (let i = 0; i < this.steps.length; i++) { | ||||
|             const step = this.steps[i]; | ||||
|             let r = step.convert(state, json, context + "(fusion " + this.constructor.name + "." + i + ")") | ||||
|             errors.push(...r.errors) | ||||
|             warnings.push(...r.warnings) | ||||
|             json = r.result | ||||
|             if (errors.length > 0) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         return { | ||||
|             result: json, | ||||
|             errors, | ||||
|             warnings | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| class AddMiniMap extends DesugaringStep<LayerConfigJson> { | ||||
|     constructor() { | ||||
|         super("Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", ["tagRenderings"]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns true if this tag rendering has a minimap in some language. | ||||
|      * Note: this minimap can be hidden by conditions | ||||
|      */ | ||||
|     private static hasMinimap(renderingConfig: TagRenderingConfigJson): boolean { | ||||
|         const translations: Translation[] = Utils.NoNull([renderingConfig.render, ...(renderingConfig.mappings ?? []).map(m => m.then)]); | ||||
|         for (const translation of translations) { | ||||
|             for (const key in translation.translations) { | ||||
|                 if (!translation.translations.hasOwnProperty(key)) { | ||||
|                     continue | ||||
|                 } | ||||
|                 const template = translation.translations[key] | ||||
|                 const parts = SubstitutedTranslation.ExtractSpecialComponents(template) | ||||
|                 const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap") | ||||
|                 if (hasMiniMap) { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
|      | ||||
|     convert(state: DesugaringContext, layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } { | ||||
|          | ||||
| 
 | ||||
| 
 | ||||
|         const hasMinimap = layerConfig.tagRenderings?.some(tr => AddMiniMap.hasMinimap(<TagRenderingConfigJson> tr)) ?? true | ||||
|         if (!hasMinimap) { | ||||
|             layerConfig = {...layerConfig} | ||||
|             layerConfig.tagRenderings = [...layerConfig.tagRenderings] | ||||
|             layerConfig.tagRenderings.push(state.tagRenderings.get("minimap")) | ||||
|         } | ||||
|          | ||||
|         return { | ||||
|             errors:[], | ||||
|             warnings: [], | ||||
|             result: layerConfig | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> { | ||||
|     constructor() { | ||||
|         super("Converts a tagRenderingSpec into the full tagRendering", []); | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } { | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
| 
 | ||||
|         return { | ||||
|             result: this.convertUntilStable(state, json, warnings, errors, context), | ||||
|             errors, warnings | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     private lookup(state: DesugaringContext, name: string): TagRenderingConfigJson[] { | ||||
|         if (state.tagRenderings.has(name)) { | ||||
|             return [state.tagRenderings.get(name)] | ||||
|         } | ||||
|         if (name.indexOf(".") >= 0) { | ||||
|             const spl = name.split("."); | ||||
|             const layer = state.sharedLayers.get(spl[0]) | ||||
|             if (spl.length === 2 && layer !== undefined) { | ||||
|                 const id = spl[1]; | ||||
| 
 | ||||
|                 const layerTrs = <TagRenderingConfigJson[]>layer.tagRenderings.filter(tr => tr["id"] !== undefined) | ||||
|                 let matchingTrs: TagRenderingConfigJson[] | ||||
|                 if (id === "*") { | ||||
|                     matchingTrs = layerTrs | ||||
|                 } else if (id.startsWith("*")) { | ||||
|                     const id_ = id.substring(1) | ||||
|                     matchingTrs = layerTrs.filter(tr => tr.group === id_) | ||||
|                 } else { | ||||
|                     matchingTrs = layerTrs.filter(tr => tr.id === id) | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 for (let i = 0; i < matchingTrs.length; i++) { | ||||
|                     // The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown
 | ||||
|                     const found = Utils.Clone(matchingTrs[i]); | ||||
|                     if (found.condition === undefined) { | ||||
|                         found.condition = layer.source.osmTags | ||||
|                     } else { | ||||
|                         found.condition = {and: [found.condition, layer.source.osmTags]} | ||||
|                     } | ||||
|                     matchingTrs[i] = found | ||||
|                 } | ||||
| 
 | ||||
|                 if (matchingTrs.length !== 0) { | ||||
|                     return matchingTrs | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return undefined; | ||||
|     } | ||||
| 
 | ||||
|     private convertOnce(state: DesugaringContext, tr: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] { | ||||
|         if (tr === "questions") { | ||||
|             return [{ | ||||
|                 id: "questions" | ||||
|             }] | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         if (typeof tr === "string") { | ||||
|             const lookup = this.lookup(state, tr); | ||||
|             if (lookup !== undefined) { | ||||
|                 return lookup | ||||
|             } | ||||
|             warnings.push(ctx + "A literal rendering was detected: " + tr) | ||||
|             return [{ | ||||
|                 render: tr, | ||||
|                 id: tr.replace(/![a-zA-Z0-9]/g, "") | ||||
|             }] | ||||
|         } | ||||
| 
 | ||||
|         if (tr["builtin"] !== undefined) { | ||||
|             let names = tr["builtin"] | ||||
|             if (typeof names === "string") { | ||||
|                 names = [names] | ||||
|             } | ||||
| 
 | ||||
|             for (const key of Object.keys(tr)) { | ||||
|                 if (key === "builtin" || key === "override" || key === "id" || key.startsWith("#")) { | ||||
|                     continue | ||||
|                 } | ||||
|                 errors.push("At " + ctx + ": an object calling a builtin can only have keys `builtin` or `override`, but a key with name `" + key + "` was found. This won't be picked up! The full object is: " + JSON.stringify(tr)) | ||||
|             } | ||||
| 
 | ||||
|             const trs: TagRenderingConfigJson[] = [] | ||||
|             for (const name of names) { | ||||
|                 const lookup = this.lookup(state, name) | ||||
|                 if (lookup === undefined) { | ||||
|                     errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " + Array.from(state.tagRenderings.keys()).join(", ") + "?") | ||||
|                     continue | ||||
|                 } | ||||
|                 for (let foundTr of lookup) { | ||||
|                     foundTr = Utils.Clone<any>(foundTr) | ||||
|                     Utils.Merge(tr["override"] ?? {}, foundTr) | ||||
|                     trs.push(foundTr) | ||||
|                 } | ||||
|             } | ||||
|             return trs; | ||||
|         } | ||||
| 
 | ||||
|         return [tr] | ||||
|     } | ||||
| 
 | ||||
|     private convertUntilStable(state: DesugaringContext, spec: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] { | ||||
|         const trs = this.convertOnce(state, spec, warnings, errors, ctx); | ||||
| 
 | ||||
|         const result = [] | ||||
|         for (const tr of trs) { | ||||
|             if (tr["builtin"] !== undefined) { | ||||
|                 const stable = this.convertUntilStable(state, tr, warnings, errors, ctx + "(RECURSIVE RESOLVE)") | ||||
|                 result.push(...stable) | ||||
|             } else { | ||||
|                 result.push(tr) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class ExpandGroupRewrite extends Conversion<{ | ||||
|     rewrite: { | ||||
|         sourceString: string, | ||||
|         into: string[] | ||||
|     }[], | ||||
|     renderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[] | ||||
| } | TagRenderingConfigJson, TagRenderingConfigJson[]> { | ||||
| 
 | ||||
| 
 | ||||
|     private static expandSubTagRenderings = new ExpandTagRendering() | ||||
| 
 | ||||
|     constructor() { | ||||
|         super( | ||||
|             "Converts a rewrite config for tagRenderings into the expanded form" | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: | ||||
|         { | ||||
|             rewrite: | ||||
|                 { sourceString: string; into: string[] }[]; renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[] | ||||
|         } | TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } { | ||||
| 
 | ||||
|         if (json["rewrite"] === undefined) { | ||||
|             return {result: [<TagRenderingConfigJson>json], errors: [], warnings: []} | ||||
|         } | ||||
|         let config = <{ | ||||
|             rewrite: | ||||
|                 { sourceString: string; into: string[] }[]; | ||||
|             renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[] | ||||
|         }>json; | ||||
| 
 | ||||
| 
 | ||||
|         const subRenderingsRes = ExpandGroupRewrite.expandSubTagRenderings.convertAll(state, config.renderings, context); | ||||
|         const subRenderings: TagRenderingConfigJson[] = [].concat(subRenderingsRes.result); | ||||
|         const errors = subRenderingsRes.errors; | ||||
|         const warnings = subRenderingsRes.warnings; | ||||
| 
 | ||||
| 
 | ||||
|         const rewrittenPerGroup = new Map<string, TagRenderingConfigJson[]>() | ||||
| 
 | ||||
|         // The actual rewriting
 | ||||
|         for (const rewrite of config.rewrite) { | ||||
|             const source = rewrite.sourceString; | ||||
|             for (const target of rewrite.into) { | ||||
|                 const groupName = target; | ||||
|                 const trs: TagRenderingConfigJson[] = [] | ||||
| 
 | ||||
|                 for (const tr of subRenderings) { | ||||
|                     trs.push(this.prepConfig(source, target, tr)) | ||||
|                 } | ||||
|                 if (rewrittenPerGroup.has(groupName)) { | ||||
|                     rewrittenPerGroup.get(groupName).push(...trs) | ||||
| 
 | ||||
|                 } else { | ||||
|                     rewrittenPerGroup.set(groupName, trs) | ||||
| 
 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Add questions box for this category
 | ||||
|         rewrittenPerGroup.forEach((group, groupName) => { | ||||
|             group.push(<TagRenderingConfigJson>{ | ||||
|                 id: "questions", | ||||
|                 group: groupName | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         rewrittenPerGroup.forEach((group, _) => { | ||||
|             group.forEach(tr => { | ||||
|                 if (tr.id === undefined || tr.id === "") { | ||||
|                     errors.push("A tagrendering has an empty ID after expanding the tag") | ||||
|                 } | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         return { | ||||
|             result: [].concat(...Array.from(rewrittenPerGroup.values())), | ||||
|             errors, warnings | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /* Used for left|right group creation and replacement */ | ||||
|     private prepConfig(keyToRewrite: string, target: string, tr: TagRenderingConfigJson) { | ||||
| 
 | ||||
|         function replaceRecursive(transl: string | any) { | ||||
|             if (typeof transl === "string") { | ||||
|                 return transl.replace(keyToRewrite, target) | ||||
|             } | ||||
|             if (transl.map !== undefined) { | ||||
|                 return transl.map(o => replaceRecursive(o)) | ||||
|             } | ||||
|             transl = {...transl} | ||||
|             for (const key in transl) { | ||||
|                 transl[key] = replaceRecursive(transl[key]) | ||||
|             } | ||||
|             return transl | ||||
|         } | ||||
| 
 | ||||
|         const orig = tr; | ||||
|         tr = replaceRecursive(tr) | ||||
| 
 | ||||
|         tr.id = target + "-" + orig.id | ||||
|         tr.group = target | ||||
|         return tr | ||||
|     } | ||||
| } | ||||
| import {DesugaringContext, DesugaringStep, Fuse, OnEvery} from "./Conversion"; | ||||
| 
 | ||||
| 
 | ||||
| export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string | { builtin, override }> { | ||||
|  | @ -822,235 +395,3 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> { | ||||
|     constructor() { | ||||
|         super("If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)", ["layers"]); | ||||
|     } | ||||
| 
 | ||||
|     private static CalculateDependencies(alreadyLoaded: LayerConfigJson[], allKnownLayers: Map<string, LayerConfigJson>, themeId: string): LayerConfigJson[] { | ||||
|         const dependenciesToAdd: LayerConfigJson[] = [] | ||||
|         const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map(l => l.id)); | ||||
| 
 | ||||
|         // Verify cross-dependencies
 | ||||
|         let unmetDependencies: { neededLayer: string, neededBy: string, reason: string, context?: string }[] = [] | ||||
|         do { | ||||
|             const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = [] | ||||
| 
 | ||||
|             for (const layerConfig of alreadyLoaded) { | ||||
|                 const layerDeps = DependencyCalculator.getLayerDependencies(new LayerConfig(layerConfig)) | ||||
|                 dependencies.push(...layerDeps) | ||||
|             } | ||||
| 
 | ||||
|             // During the generate script, builtin layers are verified but not loaded - so we have to add them manually here
 | ||||
|             // Their existance is checked elsewhere, so this is fine
 | ||||
|             unmetDependencies = dependencies.filter(dep => !loadedLayerIds.has(dep.neededLayer)) | ||||
|             for (const unmetDependency of unmetDependencies) { | ||||
|                 if (loadedLayerIds.has(unmetDependency.neededLayer)) { | ||||
|                     continue | ||||
|                 } | ||||
|                 const dep = allKnownLayers.get(unmetDependency.neededLayer) | ||||
|                 if (dep === undefined) { | ||||
|                     const message = | ||||
|                         ["Loading a dependency failed: layer " + unmetDependency.neededLayer + " is not found, neither as layer of " + themeId + " nor as builtin layer.", | ||||
|                             "This layer is needed by " + unmetDependency.neededBy, | ||||
|                             unmetDependency.reason + " (at " + unmetDependency.context + ")", | ||||
|                             "Loaded layers are: " + alreadyLoaded.map(l => l.id).join(",") | ||||
| 
 | ||||
|                         ] | ||||
|                     throw message.join("\n\t"); | ||||
|                 } | ||||
|                 dependenciesToAdd.unshift(dep) | ||||
|                 loadedLayerIds.add(dep.id); | ||||
|                 unmetDependencies = unmetDependencies.filter(d => d.neededLayer !== unmetDependency.neededLayer) | ||||
|             } | ||||
| 
 | ||||
|         } while (unmetDependencies.length > 0) | ||||
| 
 | ||||
|         return dependenciesToAdd; | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, theme: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { | ||||
|         const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers; | ||||
|         const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings; | ||||
|         const errors = []; | ||||
|         const warnings = []; | ||||
|         const layers: LayerConfigJson[] = <LayerConfigJson[]> theme.layers; // Layers should be expanded at this point
 | ||||
|          | ||||
|         knownTagRenderings.forEach((value, key) => { | ||||
|             value.id = key; | ||||
|         }) | ||||
| 
 | ||||
|         const dependencies = AddDependencyLayersToTheme.CalculateDependencies(layers, allKnownLayers, theme.id); | ||||
|         if (dependencies.length > 0) { | ||||
| 
 | ||||
|             warnings.push(context + ": added " + dependencies.map(d => d.id).join(", ") + " to the theme as they are needed") | ||||
|         } | ||||
|         layers.unshift(...dependencies); | ||||
| 
 | ||||
|         return { | ||||
|             result: { | ||||
|                 ...theme, | ||||
|                 layers: layers | ||||
|             }, | ||||
|             errors, | ||||
|             warnings | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class SetDefault<T> extends DesugaringStep<T> { | ||||
|     private readonly value: any; | ||||
|     private readonly key: string; | ||||
|     private readonly _overrideEmptyString: boolean; | ||||
| 
 | ||||
|     constructor(key: string, value: any, overrideEmptyString = false) { | ||||
|         super("Sets " + key + " to a default value if undefined"); | ||||
|         this.key = key; | ||||
|         this.value = value; | ||||
|         this._overrideEmptyString = overrideEmptyString; | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } { | ||||
|         if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) { | ||||
|             json = {...json} | ||||
|             json[this.key] = this.value | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             errors: [], warnings: [], | ||||
|             result: json | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class PrepareLayer extends Fuse<LayerConfigJson> { | ||||
|     constructor() { | ||||
|         super( | ||||
|             "Fully prepares and expands a layer for the LayerConfig.", | ||||
|             new OnEveryConcat("tagRenderings", new ExpandGroupRewrite()), | ||||
|             new OnEveryConcat("tagRenderings", new ExpandTagRendering()), | ||||
|             new SetDefault("titleIcons", ["defaults"]), | ||||
|             new OnEveryConcat("titleIcons", new ExpandTagRendering()) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfigJson[]> { | ||||
|     constructor() { | ||||
|         super("Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", []); | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[]; warnings: string[] } { | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
|         if (typeof json === "string") { | ||||
|             const found = state.sharedLayers.get(json) | ||||
|             if (found === undefined) { | ||||
|                 return { | ||||
|                     result: null, | ||||
|                     errors: [context + ": The layer with name " + json + " was not found as a builtin layer"], | ||||
|                     warnings | ||||
|                 } | ||||
|             } | ||||
|             return { | ||||
|                 result: [found], | ||||
|                 errors, warnings | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (json["builtin"] !== undefined) { | ||||
|             let names = json["builtin"] | ||||
|             if (typeof names === "string") { | ||||
|                 names = [names] | ||||
|             } | ||||
|             const layers = [] | ||||
|             for (const name of names) { | ||||
|                 const found = Utils.Clone(state.sharedLayers.get(name)) | ||||
|                 if (found === undefined) { | ||||
|                     errors.push(context + ": The layer with name " + json + " was not found as a builtin layer") | ||||
|                     continue | ||||
|                 } | ||||
|                 if (json["override"]["tagRenderings"] !== undefined && (found["tagRenderings"] ?? []).length > 0) { | ||||
|                     errors.push(`At ${context}: when overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`) | ||||
|                 } | ||||
|                 try { | ||||
|                     Utils.Merge(json["override"], found); | ||||
|                     layers.push(found) | ||||
|                 } catch (e) { | ||||
|                     errors.push(`At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(json["override"],)}`) | ||||
|                 } | ||||
|             } | ||||
|             return { | ||||
|                 result: layers, | ||||
|                 errors, warnings | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             result: [json], | ||||
|             errors, warnings | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> { | ||||
| 
 | ||||
|     constructor() { | ||||
|         super("Adds the default layers, namely: " + Constants.added_by_default.join(", "), ["layers"]); | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
|         json.layers = [...json.layers] | ||||
| 
 | ||||
|         if (json.id === "personal") { | ||||
|             json.layers = [] | ||||
|             for (const publicLayer of AllKnownLayouts.AllPublicLayers()) { | ||||
|                 const id = publicLayer.id | ||||
|                 const config = state.sharedLayers.get(id) | ||||
|                 if(Constants.added_by_default.indexOf(id) >= 0){ | ||||
|                     continue; | ||||
|                 } | ||||
|                 if(config === undefined){ | ||||
|                     // This is a layer which is coded within a public theme, not as separate .json
 | ||||
|                     continue | ||||
|                 } | ||||
|                 json.layers.push(config) | ||||
|             } | ||||
|             const publicIds = AllKnownLayouts.AllPublicLayers().map(l => l.id) | ||||
|             publicIds.map(id => state.sharedLayers.get(id)) | ||||
|         } | ||||
| 
 | ||||
|         for (const layerName of Constants.added_by_default) { | ||||
|             const v = state.sharedLayers.get(layerName) | ||||
|             if (v === undefined) { | ||||
|                 errors.push("Default layer " + layerName + " not found") | ||||
|             } | ||||
|             json.layers.push(v) | ||||
|         } | ||||
|          | ||||
|         return { | ||||
|             result: json, | ||||
|             errors, | ||||
|             warnings | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class PrepareTheme extends Fuse<LayoutConfigJson> { | ||||
|     constructor() { | ||||
|         super( | ||||
|             "Fully prepares and expands a theme", | ||||
|             new OnEveryConcat("layers", new SubstituteLayer()), | ||||
|             new SetDefault("socialImage", "assets/SocialImage.png", true), | ||||
|             new AddDefaultLayers(), | ||||
|             new AddDependencyLayersToTheme(), | ||||
|             new OnEvery("layers", new PrepareLayer()), | ||||
|             new OnEvery("layers", new AddMiniMap()) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										252
									
								
								Models/ThemeConfig/Conversion/PrepareLayer.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								Models/ThemeConfig/Conversion/PrepareLayer.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,252 @@ | |||
| import {Conversion, DesugaringContext, Fuse, OnEveryConcat, SetDefault} from "./Conversion"; | ||||
| import {LayerConfigJson} from "../Json/LayerConfigJson"; | ||||
| import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| 
 | ||||
| class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> { | ||||
|     constructor() { | ||||
|         super("Converts a tagRenderingSpec into the full tagRendering", []); | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } { | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
| 
 | ||||
|         return { | ||||
|             result: this.convertUntilStable(state, json, warnings, errors, context), | ||||
|             errors, warnings | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     private lookup(state: DesugaringContext, name: string): TagRenderingConfigJson[] { | ||||
|         if (state.tagRenderings.has(name)) { | ||||
|             return [state.tagRenderings.get(name)] | ||||
|         } | ||||
|         if (name.indexOf(".") >= 0) { | ||||
|             const spl = name.split("."); | ||||
|             const layer = state.sharedLayers.get(spl[0]) | ||||
|             if (spl.length === 2 && layer !== undefined) { | ||||
|                 const id = spl[1]; | ||||
| 
 | ||||
|                 const layerTrs = <TagRenderingConfigJson[]>layer.tagRenderings.filter(tr => tr["id"] !== undefined) | ||||
|                 let matchingTrs: TagRenderingConfigJson[] | ||||
|                 if (id === "*") { | ||||
|                     matchingTrs = layerTrs | ||||
|                 } else if (id.startsWith("*")) { | ||||
|                     const id_ = id.substring(1) | ||||
|                     matchingTrs = layerTrs.filter(tr => tr.group === id_) | ||||
|                 } else { | ||||
|                     matchingTrs = layerTrs.filter(tr => tr.id === id) | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 for (let i = 0; i < matchingTrs.length; i++) { | ||||
|                     // The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown
 | ||||
|                     const found = Utils.Clone(matchingTrs[i]); | ||||
|                     if (found.condition === undefined) { | ||||
|                         found.condition = layer.source.osmTags | ||||
|                     } else { | ||||
|                         found.condition = {and: [found.condition, layer.source.osmTags]} | ||||
|                     } | ||||
|                     matchingTrs[i] = found | ||||
|                 } | ||||
| 
 | ||||
|                 if (matchingTrs.length !== 0) { | ||||
|                     return matchingTrs | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return undefined; | ||||
|     } | ||||
| 
 | ||||
|     private convertOnce(state: DesugaringContext, tr: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] { | ||||
|         if (tr === "questions") { | ||||
|             return [{ | ||||
|                 id: "questions" | ||||
|             }] | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         if (typeof tr === "string") { | ||||
|             const lookup = this.lookup(state, tr); | ||||
|             if (lookup !== undefined) { | ||||
|                 return lookup | ||||
|             } | ||||
|             warnings.push(ctx + "A literal rendering was detected: " + tr) | ||||
|             return [{ | ||||
|                 render: tr, | ||||
|                 id: tr.replace(/![a-zA-Z0-9]/g, "") | ||||
|             }] | ||||
|         } | ||||
| 
 | ||||
|         if (tr["builtin"] !== undefined) { | ||||
|             let names = tr["builtin"] | ||||
|             if (typeof names === "string") { | ||||
|                 names = [names] | ||||
|             } | ||||
| 
 | ||||
|             for (const key of Object.keys(tr)) { | ||||
|                 if (key === "builtin" || key === "override" || key === "id" || key.startsWith("#")) { | ||||
|                     continue | ||||
|                 } | ||||
|                 errors.push("At " + ctx + ": an object calling a builtin can only have keys `builtin` or `override`, but a key with name `" + key + "` was found. This won't be picked up! The full object is: " + JSON.stringify(tr)) | ||||
|             } | ||||
| 
 | ||||
|             const trs: TagRenderingConfigJson[] = [] | ||||
|             for (const name of names) { | ||||
|                 const lookup = this.lookup(state, name) | ||||
|                 if (lookup === undefined) { | ||||
|                     errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " + Array.from(state.tagRenderings.keys()).join(", ") + "?") | ||||
|                     continue | ||||
|                 } | ||||
|                 for (let foundTr of lookup) { | ||||
|                     foundTr = Utils.Clone<any>(foundTr) | ||||
|                     Utils.Merge(tr["override"] ?? {}, foundTr) | ||||
|                     trs.push(foundTr) | ||||
|                 } | ||||
|             } | ||||
|             return trs; | ||||
|         } | ||||
| 
 | ||||
|         return [tr] | ||||
|     } | ||||
| 
 | ||||
|     private convertUntilStable(state: DesugaringContext, spec: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] { | ||||
|         const trs = this.convertOnce(state, spec, warnings, errors, ctx); | ||||
| 
 | ||||
|         const result = [] | ||||
|         for (const tr of trs) { | ||||
|             if (tr["builtin"] !== undefined) { | ||||
|                 const stable = this.convertUntilStable(state, tr, warnings, errors, ctx + "(RECURSIVE RESOLVE)") | ||||
|                 result.push(...stable) | ||||
|             } else { | ||||
|                 result.push(tr) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class ExpandGroupRewrite extends Conversion<{ | ||||
|     rewrite: { | ||||
|         sourceString: string, | ||||
|         into: string[] | ||||
|     }[], | ||||
|     renderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[] | ||||
| } | TagRenderingConfigJson, TagRenderingConfigJson[]> { | ||||
| 
 | ||||
| 
 | ||||
|     private static expandSubTagRenderings = new ExpandTagRendering() | ||||
| 
 | ||||
|     constructor() { | ||||
|         super( | ||||
|             "Converts a rewrite config for tagRenderings into the expanded form" | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: | ||||
|         { | ||||
|             rewrite: | ||||
|                 { sourceString: string; into: string[] }[]; renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[] | ||||
|         } | TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } { | ||||
| 
 | ||||
|         if (json["rewrite"] === undefined) { | ||||
|             return {result: [<TagRenderingConfigJson>json], errors: [], warnings: []} | ||||
|         } | ||||
|         let config = <{ | ||||
|             rewrite: | ||||
|                 { sourceString: string; into: string[] }[]; | ||||
|             renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[] | ||||
|         }>json; | ||||
| 
 | ||||
| 
 | ||||
|         const subRenderingsRes = ExpandGroupRewrite.expandSubTagRenderings.convertAll(state, config.renderings, context); | ||||
|         const subRenderings: TagRenderingConfigJson[] = [].concat(subRenderingsRes.result); | ||||
|         const errors = subRenderingsRes.errors; | ||||
|         const warnings = subRenderingsRes.warnings; | ||||
| 
 | ||||
| 
 | ||||
|         const rewrittenPerGroup = new Map<string, TagRenderingConfigJson[]>() | ||||
| 
 | ||||
|         // The actual rewriting
 | ||||
|         for (const rewrite of config.rewrite) { | ||||
|             const source = rewrite.sourceString; | ||||
|             for (const target of rewrite.into) { | ||||
|                 const groupName = target; | ||||
|                 const trs: TagRenderingConfigJson[] = [] | ||||
| 
 | ||||
|                 for (const tr of subRenderings) { | ||||
|                     trs.push(this.prepConfig(source, target, tr)) | ||||
|                 } | ||||
|                 if (rewrittenPerGroup.has(groupName)) { | ||||
|                     rewrittenPerGroup.get(groupName).push(...trs) | ||||
| 
 | ||||
|                 } else { | ||||
|                     rewrittenPerGroup.set(groupName, trs) | ||||
| 
 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Add questions box for this category
 | ||||
|         rewrittenPerGroup.forEach((group, groupName) => { | ||||
|             group.push(<TagRenderingConfigJson>{ | ||||
|                 id: "questions", | ||||
|                 group: groupName | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         rewrittenPerGroup.forEach((group, _) => { | ||||
|             group.forEach(tr => { | ||||
|                 if (tr.id === undefined || tr.id === "") { | ||||
|                     errors.push("A tagrendering has an empty ID after expanding the tag") | ||||
|                 } | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         return { | ||||
|             result: [].concat(...Array.from(rewrittenPerGroup.values())), | ||||
|             errors, warnings | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /* Used for left|right group creation and replacement */ | ||||
|     private prepConfig(keyToRewrite: string, target: string, tr: TagRenderingConfigJson) { | ||||
| 
 | ||||
|         function replaceRecursive(transl: string | any) { | ||||
|             if (typeof transl === "string") { | ||||
|                 return transl.replace(keyToRewrite, target) | ||||
|             } | ||||
|             if (transl.map !== undefined) { | ||||
|                 return transl.map(o => replaceRecursive(o)) | ||||
|             } | ||||
|             transl = {...transl} | ||||
|             for (const key in transl) { | ||||
|                 transl[key] = replaceRecursive(transl[key]) | ||||
|             } | ||||
|             return transl | ||||
|         } | ||||
| 
 | ||||
|         const orig = tr; | ||||
|         tr = replaceRecursive(tr) | ||||
| 
 | ||||
|         tr.id = target + "-" + orig.id | ||||
|         tr.group = target | ||||
|         return tr | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export class PrepareLayer extends Fuse<LayerConfigJson> { | ||||
|     constructor() { | ||||
|         super( | ||||
|             "Fully prepares and expands a layer for the LayerConfig.", | ||||
|             new OnEveryConcat("tagRenderings", new ExpandGroupRewrite()), | ||||
|             new OnEveryConcat("tagRenderings", new ExpandTagRendering()), | ||||
|             new SetDefault("titleIcons", ["defaults"]), | ||||
|             new OnEveryConcat("titleIcons", new ExpandTagRendering()) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										316
									
								
								Models/ThemeConfig/Conversion/PrepareTheme.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										316
									
								
								Models/ThemeConfig/Conversion/PrepareTheme.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,316 @@ | |||
| import {Conversion, DesugaringContext, DesugaringStep, Fuse, OnEvery, OnEveryConcat, SetDefault} from "./Conversion"; | ||||
| import {LayoutConfigJson} from "../Json/LayoutConfigJson"; | ||||
| import {PrepareLayer} from "./PrepareLayer"; | ||||
| import {LayerConfigJson} from "../Json/LayerConfigJson"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import Constants from "../../Constants"; | ||||
| import {AllKnownLayouts} from "../../../Customizations/AllKnownLayouts"; | ||||
| import CreateNoteImportLayer from "./CreateNoteImportLayer"; | ||||
| import LayerConfig from "../LayerConfig"; | ||||
| import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"; | ||||
| import {Translation} from "../../../UI/i18n/Translation"; | ||||
| import {SubstitutedTranslation} from "../../../UI/SubstitutedTranslation"; | ||||
| import DependencyCalculator from "../DependencyCalculator"; | ||||
| 
 | ||||
| class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfigJson[]> { | ||||
|     constructor() { | ||||
|         super("Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", []); | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[]; warnings: string[] } { | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
|         if (typeof json === "string") { | ||||
|             const found = state.sharedLayers.get(json) | ||||
|             if (found === undefined) { | ||||
|                 return { | ||||
|                     result: null, | ||||
|                     errors: [context + ": The layer with name " + json + " was not found as a builtin layer"], | ||||
|                     warnings | ||||
|                 } | ||||
|             } | ||||
|             return { | ||||
|                 result: [found], | ||||
|                 errors, warnings | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (json["builtin"] !== undefined) { | ||||
|             let names = json["builtin"] | ||||
|             if (typeof names === "string") { | ||||
|                 names = [names] | ||||
|             } | ||||
|             const layers = [] | ||||
|             for (const name of names) { | ||||
|                 const found = Utils.Clone(state.sharedLayers.get(name)) | ||||
|                 if (found === undefined) { | ||||
|                     errors.push(context + ": The layer with name " + json + " was not found as a builtin layer") | ||||
|                     continue | ||||
|                 } | ||||
|                 if (json["override"]["tagRenderings"] !== undefined && (found["tagRenderings"] ?? []).length > 0) { | ||||
|                     errors.push(`At ${context}: when overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`) | ||||
|                 } | ||||
|                 try { | ||||
|                     Utils.Merge(json["override"], found); | ||||
|                     layers.push(found) | ||||
|                 } catch (e) { | ||||
|                     errors.push(`At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(json["override"],)}`) | ||||
|                 } | ||||
|             } | ||||
|             return { | ||||
|                 result: layers, | ||||
|                 errors, warnings | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             result: [json], | ||||
|             errors, warnings | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> { | ||||
| 
 | ||||
|     constructor() { | ||||
|         super("Adds the default layers, namely: " + Constants.added_by_default.join(", "), ["layers"]); | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
|         json.layers = [...json.layers] | ||||
| 
 | ||||
|         if (json.id === "personal") { | ||||
|             json.layers = [] | ||||
|             for (const publicLayer of AllKnownLayouts.AllPublicLayers()) { | ||||
|                 const id = publicLayer.id | ||||
|                 const config = state.sharedLayers.get(id) | ||||
|                 if (Constants.added_by_default.indexOf(id) >= 0) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (config === undefined) { | ||||
|                     // This is a layer which is coded within a public theme, not as separate .json
 | ||||
|                     continue | ||||
|                 } | ||||
|                 json.layers.push(config) | ||||
|             } | ||||
|             const publicIds = AllKnownLayouts.AllPublicLayers().map(l => l.id) | ||||
|             publicIds.map(id => state.sharedLayers.get(id)) | ||||
|         } | ||||
| 
 | ||||
|         for (const layerName of Constants.added_by_default) { | ||||
|             const v = state.sharedLayers.get(layerName) | ||||
|             if (v === undefined) { | ||||
|                 errors.push("Default layer " + layerName + " not found") | ||||
|             } | ||||
|             json.layers.push(v) | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             result: json, | ||||
|             errors, | ||||
|             warnings | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| class AddImportLayers extends DesugaringStep<LayoutConfigJson> { | ||||
|     constructor() { | ||||
|         super("For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)", ["layers"]); | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
|        | ||||
|         json = {...json} | ||||
|         const allLayers: LayerConfigJson[] = <LayerConfigJson[]>json.layers; | ||||
|         json.layers = [...json.layers] | ||||
| 
 | ||||
|        | ||||
|         const creator = new CreateNoteImportLayer() | ||||
|         for (let i1 = 0; i1 < allLayers.length; i1++) { | ||||
|             const layer = allLayers[i1]; | ||||
|             if (Constants.priviliged_layers.indexOf(layer.id) >= 0) { | ||||
|                 // Priviliged layers are skipped
 | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (layer.source["geoJson"] !== undefined) { | ||||
|                 // Layer which don't get their data from OSM are skipped
 | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (layer.title === undefined || layer.name === undefined) { | ||||
|                 // Anonymous layers and layers without popup are skipped
 | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (layer.presets === undefined || layer.presets.length == 0) { | ||||
|                 // A preset is needed to be able to generate a new point
 | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
| 
 | ||||
|                 const importLayerResult = creator.convert(state, layer, context + ".(noteimportlayer)[" + i1 + "]") | ||||
|                 errors.push(...importLayerResult.errors) | ||||
|                 warnings.push(...importLayerResult.warnings) | ||||
|                 if (importLayerResult.result !== undefined) { | ||||
|                     warnings.push("Added an import layer to theme " + json.id + ", namely " + importLayerResult.result.id) | ||||
|                     json.layers.push(importLayerResult.result) | ||||
|                 } | ||||
|             } catch (e) { | ||||
|                 errors.push("Could not generate an import-layer for " + layer.id + " due to " + e) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         return { | ||||
|             errors, | ||||
|             warnings, | ||||
|             result: json | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class AddMiniMap extends DesugaringStep<LayerConfigJson> { | ||||
|     constructor() { | ||||
|         super("Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", ["tagRenderings"]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns true if this tag rendering has a minimap in some language. | ||||
|      * Note: this minimap can be hidden by conditions | ||||
|      */ | ||||
|     private static hasMinimap(renderingConfig: TagRenderingConfigJson): boolean { | ||||
|         const translations: Translation[] = Utils.NoNull([renderingConfig.render, ...(renderingConfig.mappings ?? []).map(m => m.then)]); | ||||
|         for (const translation of translations) { | ||||
|             for (const key in translation.translations) { | ||||
|                 if (!translation.translations.hasOwnProperty(key)) { | ||||
|                     continue | ||||
|                 } | ||||
|                 const template = translation.translations[key] | ||||
|                 const parts = SubstitutedTranslation.ExtractSpecialComponents(template) | ||||
|                 const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap") | ||||
|                 if (hasMiniMap) { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } { | ||||
| 
 | ||||
| 
 | ||||
|         const hasMinimap = layerConfig.tagRenderings?.some(tr => AddMiniMap.hasMinimap(<TagRenderingConfigJson>tr)) ?? true | ||||
|         if (!hasMinimap) { | ||||
|             layerConfig = {...layerConfig} | ||||
|             layerConfig.tagRenderings = [...layerConfig.tagRenderings] | ||||
|             layerConfig.tagRenderings.push(state.tagRenderings.get("minimap")) | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             errors: [], | ||||
|             warnings: [], | ||||
|             result: layerConfig | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> { | ||||
|     constructor() { | ||||
|         super("If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)", ["layers"]); | ||||
|     } | ||||
| 
 | ||||
|     private static CalculateDependencies(alreadyLoaded: LayerConfigJson[], allKnownLayers: Map<string, LayerConfigJson>, themeId: string): LayerConfigJson[] { | ||||
|         const dependenciesToAdd: LayerConfigJson[] = [] | ||||
|         const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map(l => l.id)); | ||||
| 
 | ||||
|         // Verify cross-dependencies
 | ||||
|         let unmetDependencies: { neededLayer: string, neededBy: string, reason: string, context?: string }[] = [] | ||||
|         do { | ||||
|             const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = [] | ||||
| 
 | ||||
|             for (const layerConfig of alreadyLoaded) { | ||||
|                 const layerDeps = DependencyCalculator.getLayerDependencies(new LayerConfig(layerConfig)) | ||||
|                 dependencies.push(...layerDeps) | ||||
|             } | ||||
| 
 | ||||
|             // During the generate script, builtin layers are verified but not loaded - so we have to add them manually here
 | ||||
|             // Their existance is checked elsewhere, so this is fine
 | ||||
|             unmetDependencies = dependencies.filter(dep => !loadedLayerIds.has(dep.neededLayer)) | ||||
|             for (const unmetDependency of unmetDependencies) { | ||||
|                 if (loadedLayerIds.has(unmetDependency.neededLayer)) { | ||||
|                     continue | ||||
|                 } | ||||
|                 const dep = allKnownLayers.get(unmetDependency.neededLayer) | ||||
|                 if (dep === undefined) { | ||||
|                     const message = | ||||
|                         ["Loading a dependency failed: layer " + unmetDependency.neededLayer + " is not found, neither as layer of " + themeId + " nor as builtin layer.", | ||||
|                             "This layer is needed by " + unmetDependency.neededBy, | ||||
|                             unmetDependency.reason + " (at " + unmetDependency.context + ")", | ||||
|                             "Loaded layers are: " + alreadyLoaded.map(l => l.id).join(",") | ||||
| 
 | ||||
|                         ] | ||||
|                     throw message.join("\n\t"); | ||||
|                 } | ||||
|                 dependenciesToAdd.unshift(dep) | ||||
|                 loadedLayerIds.add(dep.id); | ||||
|                 unmetDependencies = unmetDependencies.filter(d => d.neededLayer !== unmetDependency.neededLayer) | ||||
|             } | ||||
| 
 | ||||
|         } while (unmetDependencies.length > 0) | ||||
| 
 | ||||
|         return dependenciesToAdd; | ||||
|     } | ||||
| 
 | ||||
|     convert(state: DesugaringContext, theme: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { | ||||
|         const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers; | ||||
|         const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings; | ||||
|         const errors = []; | ||||
|         const warnings = []; | ||||
|         const layers: LayerConfigJson[] = <LayerConfigJson[]>theme.layers; // Layers should be expanded at this point
 | ||||
| 
 | ||||
|         knownTagRenderings.forEach((value, key) => { | ||||
|             value.id = key; | ||||
|         }) | ||||
| 
 | ||||
|         const dependencies = AddDependencyLayersToTheme.CalculateDependencies(layers, allKnownLayers, theme.id); | ||||
|         if (dependencies.length > 0) { | ||||
| 
 | ||||
|             warnings.push(context + ": added " + dependencies.map(d => d.id).join(", ") + " to the theme as they are needed") | ||||
|         } | ||||
|         layers.unshift(...dependencies); | ||||
| 
 | ||||
|         return { | ||||
|             result: { | ||||
|                 ...theme, | ||||
|                 layers: layers | ||||
|             }, | ||||
|             errors, | ||||
|             warnings | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export class PrepareTheme extends Fuse<LayoutConfigJson> { | ||||
|     constructor() { | ||||
|         super( | ||||
|             "Fully prepares and expands a theme", | ||||
|             new OnEveryConcat("layers", new SubstituteLayer()), | ||||
|             new SetDefault("socialImage", "assets/SocialImage.png", true), | ||||
|             new AddDefaultLayers(), | ||||
|             new AddDependencyLayersToTheme(), | ||||
|             new OnEvery("layers", new PrepareLayer()), | ||||
|             new AddImportLayers(), | ||||
|             new OnEvery("layers", new AddMiniMap()) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | @ -1,11 +1,11 @@ | |||
| import {Translation} from "../i18n/Translation"; | ||||
| import Combine from "./Combine"; | ||||
| import Svg from "../../Svg"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| 
 | ||||
| export default class Loading extends Combine { | ||||
|     constructor(msg?: Translation | string) { | ||||
|         const t = Translations.T(msg) ?? Translations.t.general.loading.Clone(); | ||||
|     constructor(msg?: BaseUIElement | string) { | ||||
|         const t = Translations.W(msg) ?? Translations.t.general.loading; | ||||
|         t.SetClass("pl-2") | ||||
|         super([ | ||||
|             Svg.loading_svg().SetClass("animate-spin").SetStyle("width: 1.5rem; height: 1.5rem;"), | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ export default class Minimap { | |||
|     /** | ||||
|      * Construct a minimap | ||||
|      */ | ||||
|     public static createMiniMap: (options: MinimapOptions) => (BaseUIElement & MinimapObj) = (_) => { | ||||
|     public static createMiniMap: (options?: MinimapOptions) => (BaseUIElement & MinimapObj) = (_) => { | ||||
|         throw "CreateMinimap hasn't been initialized yet. Please call MinimapImplementation.initialize()" | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ export default class MinimapImplementation extends BaseUIElement implements Mini | |||
|     private readonly _addLayerControl: boolean; | ||||
|     private readonly _options: MinimapOptions; | ||||
| 
 | ||||
|     private constructor(options: MinimapOptions) { | ||||
|     private constructor(options?: MinimapOptions) { | ||||
|         super() | ||||
|         options = options ?? {} | ||||
|         this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined) | ||||
|  | @ -290,12 +290,6 @@ export default class MinimapImplementation extends BaseUIElement implements Mini | |||
|             map.setView([loc.lat, loc.lon], loc.zoom) | ||||
|         }) | ||||
| 
 | ||||
|         location.map(loc => loc.zoom) | ||||
|             .addCallback(zoom => { | ||||
|                 if (Math.abs(map.getZoom() - zoom) > 0.1) { | ||||
|                     map.setZoom(zoom, {}); | ||||
|                 } | ||||
|             }) | ||||
| 
 | ||||
|         if (self.bounds !== undefined) { | ||||
|             self.bounds.setData(BBox.fromLeafletBounds(map.getBounds())) | ||||
|  |  | |||
|  | @ -43,6 +43,7 @@ export default class SimpleAddUI extends Toggle { | |||
|     constructor(isShown: UIEventSource<boolean>, | ||||
|                 filterViewIsOpened: UIEventSource<boolean>, | ||||
|                 state: { | ||||
|                     featureSwitchIsTesting: UIEventSource<boolean>, | ||||
|                     layoutToUse: LayoutConfig, | ||||
|                     osmConnection: OsmConnection, | ||||
|                     changes: Changes, | ||||
|  | @ -155,6 +156,7 @@ export default class SimpleAddUI extends Toggle { | |||
| 
 | ||||
|     private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>, | ||||
|                                          state: { | ||||
|                                              featureSwitchIsTesting: UIEventSource<boolean>; | ||||
|                                              filteredLayers: UIEventSource<FilteredLayer[]>, | ||||
|                                              featureSwitchFilter: UIEventSource<boolean>, | ||||
|                                              osmConnection: OsmConnection | ||||
|  | @ -162,10 +164,9 @@ export default class SimpleAddUI extends Toggle { | |||
|         const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset) | ||||
|         let intro: BaseUIElement = Translations.t.general.add.intro; | ||||
| 
 | ||||
|         let testMode: BaseUIElement = undefined; | ||||
|         if (state.osmConnection?.userDetails?.data?.dryRun) { | ||||
|             testMode = Translations.t.general.testing.Clone().SetClass("alert") | ||||
|         } | ||||
|         let testMode: BaseUIElement = new Toggle(Translations.t.general.testing.SetClass("alert"), | ||||
|             undefined, | ||||
|             state.featureSwitchIsTesting); | ||||
| 
 | ||||
|         return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col") | ||||
| 
 | ||||
|  |  | |||
|  | @ -73,10 +73,11 @@ export default class UserBadge extends Toggle { | |||
|                     ).SetClass("alert") | ||||
|                 } | ||||
| 
 | ||||
|                 let dryrun = new FixedUiElement(""); | ||||
|                 if (user.dryRun) { | ||||
|                     dryrun = new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4"); | ||||
|                 } | ||||
|                 let dryrun = new Toggle( | ||||
|                     new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4"), | ||||
|                     undefined, | ||||
|                     state.featureSwitchIsTesting | ||||
|                 ) | ||||
| 
 | ||||
|                 const settings = | ||||
|                     new Link(Svg.gear, | ||||
|  |  | |||
							
								
								
									
										100
									
								
								UI/ImportFlow/AskMetadata.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								UI/ImportFlow/AskMetadata.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,100 @@ | |||
| import Combine from "../Base/Combine"; | ||||
| import {FlowStep} from "./FlowStep"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import ValidatedTextField from "../Input/ValidatedTextField"; | ||||
| import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; | ||||
| import Title from "../Base/Title"; | ||||
| import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts"; | ||||
| import {DropDown} from "../Input/DropDown"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| 
 | ||||
| export class AskMetadata extends Combine implements FlowStep<{ | ||||
|     features: any[], | ||||
|     wikilink: string, | ||||
|     intro: string, | ||||
|     source: string, | ||||
|     theme: string | ||||
| }> { | ||||
| 
 | ||||
|     public readonly Value: UIEventSource<{ | ||||
|         features: any[], | ||||
|         wikilink: string, | ||||
|         intro: string, | ||||
|         source: string, | ||||
|         theme: string | ||||
|     }>; | ||||
|     public readonly IsValid: UIEventSource<boolean>; | ||||
| 
 | ||||
|     constructor(params: ({ features: any[], layer: LayerConfig })) { | ||||
| 
 | ||||
|         const introduction = ValidatedTextField.InputForType("text", { | ||||
|             value: LocalStorageSource.Get("import-helper-introduction-text"), | ||||
|             inputStyle: "width: 100%" | ||||
|         }) | ||||
| 
 | ||||
|         const wikilink = ValidatedTextField.InputForType("string", { | ||||
|             value: LocalStorageSource.Get("import-helper-wikilink-text"), | ||||
|             inputStyle: "width: 100%" | ||||
|         }) | ||||
| 
 | ||||
|         const source = ValidatedTextField.InputForType("string", { | ||||
|             value: LocalStorageSource.Get("import-helper-source-text"), | ||||
|             inputStyle: "width: 100%" | ||||
|         }) | ||||
| 
 | ||||
|         let options : {value: string, shown: BaseUIElement}[]=  AllKnownLayouts.layoutsList | ||||
|             .filter(th => th.layers.some(l => l.id === params.layer.id)) | ||||
|             .filter(th => th.id !== "personal") | ||||
|             .map(th => ({ | ||||
|                 value: th.id, | ||||
|                 shown: th.title | ||||
|             })) | ||||
|          | ||||
|         options.splice(0,0, { | ||||
|             shown: new FixedUiElement("Select a theme"), | ||||
|             value:  undefined | ||||
|         }) | ||||
|          | ||||
|         const theme = new DropDown("Which theme should be linked in the note?",options) | ||||
|              | ||||
|             ValidatedTextField.InputForType("string", { | ||||
|             value: LocalStorageSource.Get("import-helper-theme-text"), | ||||
|             inputStyle: "width: 100%" | ||||
|         }) | ||||
| 
 | ||||
|         super([ | ||||
|             new Title("Set metadata"), | ||||
|             "Before adding " + params.features.length + " notes, please provide some extra information.", | ||||
|             "Please, write an introduction for someone who sees the note", | ||||
|             introduction.SetClass("w-full border border-black"), | ||||
|             "What is the source of this data? If 'source' is set in the feature, this value will be ignored", | ||||
|             source.SetClass("w-full border border-black"), | ||||
|             "On what wikipage can one find more information about this import?", | ||||
|             wikilink.SetClass("w-full border border-black"), | ||||
|             theme | ||||
|         ]); | ||||
|         this.SetClass("flex flex-col") | ||||
| 
 | ||||
|         this.Value = introduction.GetValue().map(intro => { | ||||
|             return { | ||||
|                 features: params.features, | ||||
|                 wikilink: wikilink.GetValue().data, | ||||
|                 intro, | ||||
|                 source: source.GetValue().data, | ||||
|                 theme: theme.GetValue().data | ||||
| 
 | ||||
|             } | ||||
|         }, [wikilink.GetValue(), source.GetValue(), theme.GetValue()]) | ||||
| 
 | ||||
|         this.IsValid = this.Value.map(obj => { | ||||
|             if(obj === undefined){ | ||||
|                 return false; | ||||
|             } | ||||
|             return obj.theme !== undefined && obj.features !== undefined && obj.wikilink !== undefined && obj.intro !== undefined && obj.source !== undefined; | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										134
									
								
								UI/ImportFlow/CompareToAlreadyExistingNotes.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								UI/ImportFlow/CompareToAlreadyExistingNotes.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,134 @@ | |||
| import Combine from "../Base/Combine"; | ||||
| import {FlowStep} from "./FlowStep"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {DesugaringContext} from "../../Models/ThemeConfig/Conversion/Conversion"; | ||||
| import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNoteImportLayer"; | ||||
| import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; | ||||
| import GeoJsonSource from "../../Logic/FeatureSource/Sources/GeoJsonSource"; | ||||
| import MetaTagging from "../../Logic/MetaTagging"; | ||||
| import RelationsTracker from "../../Logic/Osm/RelationsTracker"; | ||||
| import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource"; | ||||
| import Minimap from "../Base/Minimap"; | ||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | ||||
| import FeatureInfoBox from "../Popup/FeatureInfoBox"; | ||||
| import {ImportUtils} from "./ImportUtils"; | ||||
| import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json"; | ||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import Title from "../Base/Title"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import Loading from "../Base/Loading"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import * as known_layers from "../../assets/generated/known_layers.json" | ||||
| import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson"; | ||||
| 
 | ||||
| /** | ||||
|  * Filters out points for which the import-note already exists, to prevent duplicates | ||||
|  */ | ||||
| export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, geojson: any }> { | ||||
| 
 | ||||
|     public IsValid: UIEventSource<boolean> | ||||
|     public Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, geojson: any }> | ||||
| 
 | ||||
| 
 | ||||
|     constructor(state, params: { bbox: BBox, layer: LayerConfig, geojson: { features: any[] } }) { | ||||
| 
 | ||||
|         const convertState: DesugaringContext = { | ||||
|             sharedLayers: new Map(), | ||||
|             tagRenderings: new Map() | ||||
|         } | ||||
| 
 | ||||
|         const layerConfig = known_layers.filter(l => l.id === params.layer.id)[0] | ||||
|         const importLayerJson = new CreateNoteImportLayer(365).convertStrict(convertState, <LayerConfigJson> layerConfig, "CompareToAlreadyExistingNotes") | ||||
|         const importLayer = new LayerConfig(importLayerJson, "import-layer-dynamic") | ||||
|         const flayer: FilteredLayer = { | ||||
|             appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>()), | ||||
|             isDisplayed: new UIEventSource<boolean>(true), | ||||
|             layerDef: importLayer | ||||
|         } | ||||
|         const unfiltered = new GeoJsonSource(flayer, params.bbox.padAbsolute(0.0001)) | ||||
|         unfiltered.features.map(f => MetaTagging.addMetatags( | ||||
|                 f, | ||||
|                 { | ||||
|                     memberships: new RelationsTracker(), | ||||
|                     getFeaturesWithin: (layerId, bbox: BBox) => [], | ||||
|                     getFeatureById: (id: string) => undefined | ||||
|                 }, | ||||
|                 importLayer, | ||||
|                 state, | ||||
|                 { | ||||
|                     includeDates: true, | ||||
|                     // We assume that the non-dated metatags are already set by the cache generator
 | ||||
|                     includeNonDates: true | ||||
|                 } | ||||
|             ) | ||||
|         ) | ||||
|         const data = new FilteringFeatureSource(state, undefined, unfiltered) | ||||
|         data.features.addCallbackD(features => console.log("Loaded and filtered features are", features)) | ||||
|         const map = Minimap.createMiniMap() | ||||
|         map.SetClass("w-full").SetStyle("height: 500px") | ||||
| 
 | ||||
|         const comparison = Minimap.createMiniMap({ | ||||
|             location: map.location, | ||||
| 
 | ||||
|         }) | ||||
|         comparison.SetClass("w-full").SetStyle("height: 500px") | ||||
| 
 | ||||
|         new ShowDataLayer({ | ||||
|             layerToShow: importLayer, | ||||
|             state, | ||||
|             zoomToFeatures: true, | ||||
|             leafletMap: map.leafletMap, | ||||
|             features: data, | ||||
|             popup: (tags, layer) => new FeatureInfoBox(tags, layer, state) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         const maxDistance = new UIEventSource<number>(5) | ||||
| 
 | ||||
|         const partitionedImportPoints = ImportUtils.partitionFeaturesIfNearby(params.geojson, data.features | ||||
|             .map(ff => ({features: ff.map(ff => ff.feature)})), maxDistance) | ||||
| 
 | ||||
| 
 | ||||
|         new ShowDataLayer({ | ||||
|             layerToShow: new LayerConfig(import_candidate), | ||||
|             state, | ||||
|             zoomToFeatures: true, | ||||
|             leafletMap: comparison.leafletMap, | ||||
|             features: new StaticFeatureSource(partitionedImportPoints.map(p => p.hasNearby), false), | ||||
|             popup: (tags, layer) => new FeatureInfoBox(tags, layer, state) | ||||
|         }) | ||||
| 
 | ||||
|         super([ | ||||
|             new Title("Compare with already existing 'to-import'-notes"), | ||||
|             new Toggle( | ||||
|                 new Loading("Fetching notes from OSM"), | ||||
|                 new Combine([ | ||||
|                     map, | ||||
|                     "The following (red) elements are elements to import which are nearby a matching element that is already up for import. These won't be imported", | ||||
|                    | ||||
|                     new Toggle( | ||||
|                         new FixedUiElement("All of the proposed points have (or had) an import note already").SetClass("alert w-full block").SetStyle("padding: 0.5rem"), | ||||
|                         new VariableUiElement(partitionedImportPoints.map(({noNearby}) => noNearby.length + " elements can be imported")).SetClass("thanks p-8"), | ||||
|                         partitionedImportPoints.map(({noNearby}) => noNearby.length === 0) | ||||
|                     ).SetClass("w-full"), | ||||
|                     comparison, | ||||
|                 ]).SetClass("flex flex-col"), | ||||
|                 unfiltered.features.map(ff => ff === undefined || ff.length === 0) | ||||
|             ), | ||||
| 
 | ||||
| 
 | ||||
|         ]); | ||||
|         this.SetClass("flex flex-col") | ||||
|         this.Value = partitionedImportPoints.map(({noNearby}) => ({ | ||||
|             geojson: {features: noNearby, type: "FeatureCollection"}, | ||||
|             bbox: params.bbox, | ||||
|             layer: params.layer | ||||
|         })) | ||||
| 
 | ||||
|         this.IsValid = data.features.map(ff => ff.length > 0 && partitionedImportPoints.data.noNearby.length > 0, [partitionedImportPoints]) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										32
									
								
								UI/ImportFlow/ConfirmProcess.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								UI/ImportFlow/ConfirmProcess.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| import Combine from "../Base/Combine"; | ||||
| import {FlowStep} from "./FlowStep"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Link from "../Base/Link"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import CheckBoxes from "../Input/Checkboxes"; | ||||
| import Title from "../Base/Title"; | ||||
| 
 | ||||
| export class ConfirmProcess<T> extends Combine implements FlowStep<T> { | ||||
| 
 | ||||
|     public IsValid: UIEventSource<boolean> | ||||
|     public Value: UIEventSource<T> | ||||
| 
 | ||||
|     constructor(v: T) { | ||||
| 
 | ||||
|         const toConfirm = [ | ||||
|             new Combine(["I have read the ", new Link("import guidelines on the OSM wiki", "https://wiki.openstreetmap.org/wiki/Import_guidelines", true)]), | ||||
|             new FixedUiElement("I did contact the (local) community about this import"), | ||||
|             new FixedUiElement("The license of the data to import allows it to be imported into OSM. They are allowed to be redistributed commercially, with only minimal attribution"), | ||||
|             new FixedUiElement("The process is documented on the OSM-wiki (you'll need this link later)") | ||||
|         ]; | ||||
| 
 | ||||
|         const licenseClear = new CheckBoxes(toConfirm) | ||||
|         super([ | ||||
|             new Title("Did you go through the import process?"), | ||||
|             licenseClear | ||||
|         ]); | ||||
|         this.SetClass("link-underline") | ||||
|         this.IsValid = licenseClear.GetValue().map(selected => toConfirm.length == selected.length) | ||||
|         this.Value = new UIEventSource<T>(v) | ||||
|     } | ||||
| } | ||||
|  | @ -27,10 +27,12 @@ import * as currentview from "../../assets/layers/current_view/current_view.json | |||
| import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json" | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import FeatureInfoBox from "../Popup/FeatureInfoBox"; | ||||
| import {ImportUtils} from "./ImportUtils"; | ||||
| 
 | ||||
| /** | ||||
|  * Given the data to import, the bbox and the layer, will query overpass for similar items | ||||
|  */ | ||||
| export default class ConflationChecker extends Combine implements FlowStep<any> { | ||||
| export default class ConflationChecker extends Combine implements FlowStep<{features: any[], layer: LayerConfig}> { | ||||
| 
 | ||||
|     public readonly IsValid | ||||
|     public readonly Value | ||||
|  | @ -44,19 +46,21 @@ export default class ConflationChecker extends Combine implements FlowStep<any> | |||
|         const layer = params.layer; | ||||
|         const toImport = params.geojson; | ||||
|         let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached" >("idle") | ||||
|          | ||||
|         const cacheAge = new UIEventSource<number>(undefined); | ||||
|         const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, { | ||||
|             whenLoaded: (v) => { | ||||
|                 if (v !== undefined) { | ||||
|                     console.log("Loaded from local storage:", v) | ||||
|                     const [geojson, date] = v; | ||||
|                     const timeDiff = (new Date().getTime() - date.getTime()) / 1000; | ||||
|                     console.log("The cache is ", timeDiff, "seconds old") | ||||
|                     console.log("Loaded ", geojson.features.length," features; cache is ", timeDiff, "seconds old") | ||||
|                     cacheAge.setData(timeDiff) | ||||
|                     if (timeDiff < 24 * 60 * 60) { | ||||
|                         // Recently cached! 
 | ||||
|                         overpassStatus.setData("cached") | ||||
|                         return; | ||||
|                     } | ||||
|                     cacheAge.setData(-1) | ||||
|                 } | ||||
|                 // Load the data!
 | ||||
|                 const url = Constants.defaultOverpassUrls[1] | ||||
|  | @ -115,7 +119,7 @@ export default class ConflationChecker extends Combine implements FlowStep<any> | |||
|             layerToShow:new LayerConfig(currentview), | ||||
|             state, | ||||
|             leafletMap: osmLiveData.leafletMap, | ||||
|             enablePopups: undefined, | ||||
|             popup: undefined, | ||||
|             zoomToFeatures: true, | ||||
|             features: new StaticFeatureSource([ | ||||
|                 bbox.asGeoJson({}) | ||||
|  | @ -161,17 +165,10 @@ export default class ConflationChecker extends Combine implements FlowStep<any> | |||
|                 toImport.features.some(imp =>  | ||||
|                     maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) ) | ||||
|         }, [nearbyCutoff.GetValue()]), false); | ||||
|         const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number)); | ||||
| 
 | ||||
|         // Featuresource showing OSM-features which are nearby a toImport-feature 
 | ||||
|         const toImportWithNearby = new StaticFeatureSource(geojson.map(osmData => { | ||||
|             if(osmData?.features === undefined){ | ||||
|                 return [] | ||||
|             } | ||||
|             const maxDist = Number(nearbyCutoff.GetValue().data) | ||||
|             return toImport.features.filter(imp => | ||||
|                 osmData.features.some(f => | ||||
|                     maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) ) | ||||
|         }, [nearbyCutoff.GetValue()]), false); | ||||
|         const toImportWithNearby = new StaticFeatureSource(paritionedImport.map(els =>els?.hasNearby ?? []), false); | ||||
| 
 | ||||
|         new ShowDataLayer({ | ||||
|             layerToShow:layer, | ||||
|  | @ -192,6 +189,38 @@ export default class ConflationChecker extends Combine implements FlowStep<any> | |||
|         }) | ||||
|          | ||||
|          | ||||
|         const conflationMaps = new Combine([   | ||||
|             new VariableUiElement( | ||||
|             geojson.map(geojson => { | ||||
|                 if (geojson === undefined) { | ||||
|                     return undefined; | ||||
|                 } | ||||
|                 return new SubtleButton(Svg.download_svg(), "Download the loaded geojson from overpass").onClick(() => { | ||||
|                     Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, "  "), "mapcomplete-" + layer.id + ".geojson", { | ||||
|                         mimetype: "application/json+geo" | ||||
|                     }) | ||||
|                 }); | ||||
|             })), | ||||
|             new VariableUiElement(cacheAge.map(age => { | ||||
|                 if(age === undefined){ | ||||
|                     return undefined; | ||||
|                 } | ||||
|                 if(age < 0){ | ||||
|                     return new FixedUiElement("Cache was expired") | ||||
|                 } | ||||
|                 return new FixedUiElement("Loaded data is from the cache and is "+Utils.toHumanTime(age)+" old") | ||||
|             })), | ||||
| 
 | ||||
|             new Title("Live data on OSM"), | ||||
|             osmLiveData, | ||||
|             new Combine(["The live data is shown if the zoomlevel is at least ", zoomLevel, ". The current zoom level is ", new VariableUiElement(osmLiveData.location.map(l => ""+l.zoom))]).SetClass("flex"), | ||||
| 
 | ||||
|             new Title("Nearby features"), | ||||
|             new Combine([  "The following map shows features to import which have an OSM-feature within ", nearbyCutoff, "meter"]).SetClass("flex"), | ||||
|             new FixedUiElement("The red elements on the following map will <b>not</b> be imported!").SetClass("alert"), | ||||
|             "Set the range to 0 or 1 if you want to import them all", | ||||
|             matchedFeaturesMap]).SetClass("flex flex-col") | ||||
|          | ||||
|         super([ | ||||
|             new Title("Comparison with existing data"), | ||||
|             new VariableUiElement(overpassStatus.map(d => { | ||||
|  | @ -205,38 +234,19 @@ export default class ConflationChecker extends Combine implements FlowStep<any> | |||
|                     return new Loading("Querying overpass...") | ||||
|                 } | ||||
|                 if(d === "cached"){ | ||||
|                     return new FixedUiElement("Fetched data from local storage") | ||||
|                     return conflationMaps | ||||
|                 } | ||||
|                 if(d === "success"){ | ||||
|                     return new FixedUiElement("Data loaded") | ||||
|                     return conflationMaps | ||||
|                 } | ||||
|                 return new FixedUiElement("Unexpected state "+d).SetClass("alert") | ||||
|             })), | ||||
|             new VariableUiElement( | ||||
|                 geojson.map(geojson => { | ||||
|                     if (geojson === undefined) { | ||||
|                         return undefined; | ||||
|                     } | ||||
|                     return new SubtleButton(Svg.download_svg(), "Download the loaded geojson from overpass").onClick(() => { | ||||
|                         Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, "  "), "mapcomplete-" + layer.id + ".geojson", { | ||||
|                             mimetype: "application/json+geo" | ||||
|                         }) | ||||
|                     }); | ||||
|                 })), | ||||
|             })) | ||||
| 
 | ||||
|             new Title("Live data on OSM"), | ||||
|             osmLiveData, | ||||
|             new Combine(["The live data is shown if the zoomlevel is at least ", zoomLevel, ". The current zoom level is ", new VariableUiElement(osmLiveData.location.map(l => ""+l.zoom))]).SetClass("flex"), | ||||
| 
 | ||||
|             new Title("Nearby features"), | ||||
|             new Combine([  "The following map shows features to import which have an OSM-feature within ", nearbyCutoff, "meter"]).SetClass("flex"), | ||||
|             new FixedUiElement("The red elements on the following map will <b>not</b> be imported!").SetClass("alert"), | ||||
|             "Set the range to 0 or 1 if you want to import them all", | ||||
|             matchedFeaturesMap | ||||
|         ]) | ||||
| 
 | ||||
|         this.IsValid = new UIEventSource(false) | ||||
|         this.Value = new UIEventSource(undefined) | ||||
|     }  | ||||
|         this.Value = paritionedImport.map(feats => ({features: feats?.noNearby, layer: params.layer})) | ||||
|         this.Value.addCallbackAndRun(v => console.log("ConflationChecker-step value is ", v)) | ||||
|         this.IsValid = this.Value.map(v => v?.features !== undefined && v.features.length > 0) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										82
									
								
								UI/ImportFlow/CreateNotes.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								UI/ImportFlow/CreateNotes.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,82 @@ | |||
| import Combine from "../Base/Combine"; | ||||
| import {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Title from "../Base/Title"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import Loading from "../Base/Loading"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import Link from "../Base/Link"; | ||||
| 
 | ||||
| export class CreateNotes extends Combine { | ||||
| 
 | ||||
|     constructor(state: { osmConnection: OsmConnection }, v: { features: any[]; wikilink: string; intro: string; source: string, theme: string }) { | ||||
| 
 | ||||
|         const createdNotes: UIEventSource<number[]> = new UIEventSource<number[]>([]) | ||||
|         const failed = new UIEventSource<string[]>([]) | ||||
|         const currentNote = createdNotes.map(n => n.length) | ||||
| 
 | ||||
|         for (const f of v.features) { | ||||
| 
 | ||||
|             const src = f.properties["source"] ?? f.properties["src"] ?? v.source | ||||
|             delete f.properties["source"] | ||||
|             delete f.properties["src"] | ||||
| 
 | ||||
|             const tags: string [] = [] | ||||
|             for (const key in f.properties) { | ||||
|                 if(f.properties[key] === ""){ | ||||
|                     continue | ||||
|                 } | ||||
|                 tags.push(key + "=" + f.properties[key].replace(/=/, "\\=").replace(/;/g, "\\;").replace(/\n/g, "\\n")) | ||||
|             } | ||||
|             const lat = f.geometry.coordinates[1] | ||||
|             const lon = f.geometry.coordinates[0] | ||||
|             const text = [v.intro, | ||||
|                 '', | ||||
|                 "Source: " + src, | ||||
|                 'More information at ' + v.wikilink, | ||||
|                 '', | ||||
|                 'Import this point easily with', | ||||
|                 `https://mapcomplete.osm.be/${v.theme}.html?z=18&lat=${lat}&lon=${lon}#import`, | ||||
|                 ...tags].join("\n") | ||||
| 
 | ||||
|             state.osmConnection.openNote( | ||||
|                 lat, lon, text) | ||||
|                 .then(({id}) => { | ||||
|                     createdNotes.data.push(id) | ||||
|                     createdNotes.ping() | ||||
|                 }, err => { | ||||
|                     failed.data.push(err) | ||||
|                     failed.ping() | ||||
|                 }) | ||||
|         } | ||||
| 
 | ||||
|         super([ | ||||
|             new Title("Creating notes"), | ||||
|             "Hang on while we are importing...", | ||||
|             new Toggle( | ||||
|                 new Loading(new VariableUiElement(currentNote.map(count => new FixedUiElement("Imported <b>" + count + "</b> out of " + v.features.length + " notes")))), | ||||
|                 new FixedUiElement("All done!"), | ||||
|                 currentNote.map(count => count < v.features.length) | ||||
|             ), | ||||
|             new VariableUiElement(failed.map(failed => { | ||||
| 
 | ||||
|                 if (failed.length === 0) { | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return new Combine([ | ||||
|                     new FixedUiElement("Some entries failed").SetClass("alert"), | ||||
|                     ...failed | ||||
|                 ]).SetClass("flex flex-col") | ||||
| 
 | ||||
|             })), | ||||
|             new VariableUiElement(createdNotes.map(notes => { | ||||
|                 const links = notes.map(n => | ||||
|                     new Link(new FixedUiElement("https://openstreetmap.org/note/" + n), "https://openstreetmap.org/note/" + n, true)); | ||||
|                 return new Combine(links).SetClass("flex flex-col"); | ||||
|             })) | ||||
|         ]) | ||||
|         this.SetClass("flex flex-col"); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -21,7 +21,23 @@ import Table from "../Base/Table"; | |||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import {FlowStep} from "./FlowStep"; | ||||
| import {Layer} from "leaflet"; | ||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | ||||
| import {AllTagsPanel} from "../SpecialVisualizations"; | ||||
| import Title from "../Base/Title"; | ||||
| 
 | ||||
| class PreviewPanel extends ScrollableFullScreen { | ||||
|      | ||||
|     constructor(tags, layer) { | ||||
|         super( | ||||
|             _ => new FixedUiElement("Element to import"), | ||||
|             _ => new Combine(["The tags are:",  | ||||
|                 new AllTagsPanel(tags) | ||||
|             ]).SetClass("flex flex-col"), | ||||
|             "element" | ||||
|         ); | ||||
|     } | ||||
|      | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Shows the data to import on a map, asks for the correct layer to be selected | ||||
|  | @ -36,7 +52,6 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer: | |||
|         const t = Translations.t.importHelper; | ||||
| 
 | ||||
|         const propertyKeys = new Set<string>() | ||||
|         console.log("Datapanel input got ", geojson) | ||||
|         for (const f of geojson.features) { | ||||
|             Object.keys(f.properties).forEach(key => propertyKeys.add(key)) | ||||
|         } | ||||
|  | @ -56,6 +71,7 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer: | |||
|                 !layer.source.osmTags.matchesProperties(f.properties) | ||||
|             ) | ||||
|             if (!mismatched) { | ||||
|                 console.log("Autodected layer", layer.id) | ||||
|                 layerPicker.GetValue().setData(layer); | ||||
|                 layerPicker.GetValue().addCallback(_ => autodetected.setData(false)) | ||||
|                 autodetected.setData(true) | ||||
|  | @ -96,25 +112,22 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer: | |||
|         map.SetClass("w-full").SetStyle("height: 500px") | ||||
| 
 | ||||
|         new ShowDataMultiLayer({ | ||||
|             layers: new UIEventSource<FilteredLayer[]>(AllKnownLayouts.AllPublicLayers().map(l => ({ | ||||
|             layers: new UIEventSource<FilteredLayer[]>(AllKnownLayouts.AllPublicLayers() | ||||
|                 .filter(l => l.source.geojsonSource === undefined) | ||||
|                 .map(l => ({ | ||||
|                 layerDef: l, | ||||
|                 isDisplayed: new UIEventSource<boolean>(true), | ||||
|                 appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined) | ||||
|             }))), | ||||
|             zoomToFeatures: true, | ||||
|             features: new StaticFeatureSource(matching, false), | ||||
|             state: { | ||||
|                 ...state, | ||||
|                 filteredLayers: new UIEventSource<FilteredLayer[]>(undefined), | ||||
|                 backgroundLayer: background | ||||
|             }, | ||||
|             leafletMap: map.leafletMap, | ||||
| 
 | ||||
|             popup: (tag, layer) => new PreviewPanel(tag, layer).SetClass("font-lg") | ||||
|         }) | ||||
|         var bbox = matching.map(feats => BBox.bboxAroundAll(feats.map(f => new BBox([f.geometry.coordinates])))) | ||||
| 
 | ||||
|         super([ | ||||
|             "Has " + geojson.features.length + " features", | ||||
|             new Title(geojson.features.length + " features to import"), | ||||
|             layerPicker, | ||||
|             new Toggle("Automatically detected layer", undefined, autodetected), | ||||
|             new Table(["", "Key", "Values", "Unique values seen"], | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import {VariableUiElement} from "../Base/VariableUIElement"; | |||
| import Toggle from "../Input/Toggle"; | ||||
| import {UIElement} from "../UIElement"; | ||||
| 
 | ||||
| export interface FlowStep<T> extends BaseUIElement{ | ||||
| export interface FlowStep<T> extends BaseUIElement { | ||||
|     readonly IsValid: UIEventSource<boolean> | ||||
|     readonly Value: UIEventSource<T> | ||||
| } | ||||
|  | @ -16,70 +16,97 @@ export interface FlowStep<T> extends BaseUIElement{ | |||
| export class FlowPanelFactory<T> { | ||||
|     private _initial: FlowStep<any>; | ||||
|     private _steps: ((x: any) => FlowStep<any>)[]; | ||||
|     private _stepNames: string[]; | ||||
|      | ||||
|     private constructor(initial: FlowStep<any>, steps: ((x:any) => FlowStep<any>)[], stepNames: string[]) { | ||||
|     private _stepNames: (string | BaseUIElement)[]; | ||||
| 
 | ||||
|     private constructor(initial: FlowStep<any>, steps: ((x: any) => FlowStep<any>)[], stepNames: (string | BaseUIElement)[]) { | ||||
|         this._initial = initial; | ||||
|         this._steps = steps; | ||||
|         this._stepNames = stepNames; | ||||
|     } | ||||
|      | ||||
|     public static start<TOut> (step: FlowStep<TOut>): FlowPanelFactory<TOut>{ | ||||
|         return new FlowPanelFactory(step, [], []) | ||||
| 
 | ||||
|     public static start<TOut>(name: string | BaseUIElement, step: FlowStep<TOut>): FlowPanelFactory<TOut> { | ||||
|         return new FlowPanelFactory(step, [], [name]) | ||||
|     } | ||||
|      | ||||
|     public then<TOut>(name: string, construct: ((t:T) => FlowStep<TOut>)): FlowPanelFactory<TOut>{ | ||||
| 
 | ||||
|     public then<TOut>(name: string | BaseUIElement, construct: ((t: T) => FlowStep<TOut>)): FlowPanelFactory<TOut> { | ||||
|         return new FlowPanelFactory<TOut>( | ||||
|             this._initial, | ||||
|             this._steps.concat([construct]), | ||||
|             this._stepNames.concat([name]) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     public finish(construct: ((t: T, backButton?: BaseUIElement) => BaseUIElement)) : BaseUIElement { | ||||
| 
 | ||||
|     public finish(name: string | BaseUIElement, construct: ((t: T, backButton?: BaseUIElement) => BaseUIElement)): { | ||||
|         flow: BaseUIElement, | ||||
|         furthestStep: UIEventSource<number>, | ||||
|         titles: (string | BaseUIElement)[] | ||||
|     } { | ||||
|         const furthestStep = new UIEventSource(0) | ||||
|         // Construct all the flowpanels step by step (in reverse order)
 | ||||
|         const nextConstr : ((t:any, back?: UIElement) => BaseUIElement)[] = this._steps.map(_ => undefined) | ||||
|         const nextConstr: ((t: any, back?: UIElement) => BaseUIElement)[] = this._steps.map(_ => undefined) | ||||
|         nextConstr.push(construct) | ||||
|          | ||||
|         for (let i = this._steps.length - 1; i >= 0; i--){ | ||||
|             const createFlowStep : (value) => FlowStep<any> = this._steps[i]; | ||||
|         for (let i = this._steps.length - 1; i >= 0; i--) { | ||||
|             const createFlowStep: (value) => FlowStep<any> = this._steps[i]; | ||||
|             const isConfirm = i == this._steps.length - 1; | ||||
|             nextConstr[i] = (value, backButton) => { | ||||
|                 console.log("Creating flowSTep ", this._stepNames[i]) | ||||
|                 const flowStep = createFlowStep(value) | ||||
|                 return new FlowPanel(flowStep, nextConstr[i + 1], backButton); | ||||
|                 furthestStep.setData(i + 1); | ||||
|                 const panel = new FlowPanel(flowStep, nextConstr[i + 1], backButton, isConfirm); | ||||
|                 panel.isActive.addCallbackAndRun(active => { | ||||
|                     if (active) { | ||||
|                         furthestStep.setData(i + 1); | ||||
|                     } | ||||
|                 }) | ||||
|                 return panel | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         return new FlowPanel(this._initial, nextConstr[0],undefined) | ||||
| 
 | ||||
|         const flow = new FlowPanel(this._initial, nextConstr[0]) | ||||
|         flow.isActive.addCallbackAndRun(active => { | ||||
|             if (active) { | ||||
|                 furthestStep.setData(0); | ||||
|             } | ||||
|         }) | ||||
|         return { | ||||
|             flow, | ||||
|             furthestStep, | ||||
|             titles: this._stepNames | ||||
|         } | ||||
|     } | ||||
|      | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class FlowPanel<T> extends Toggle { | ||||
|      | ||||
|     public isActive: UIEventSource<boolean> | ||||
| 
 | ||||
|     constructor( | ||||
|         initial: (FlowStep<T>), | ||||
|         constructNextstep:  ((input: T, backButton: BaseUIElement) => BaseUIElement), | ||||
|         backbutton?: BaseUIElement | ||||
|         constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement), | ||||
|         backbutton?: BaseUIElement, | ||||
|         isConfirm = false | ||||
|     ) { | ||||
|         const t = Translations.t.general; | ||||
|          | ||||
| 
 | ||||
|         const currentStepActive = new UIEventSource(true); | ||||
| 
 | ||||
|         let nextStep: UIEventSource<BaseUIElement>= new UIEventSource<BaseUIElement>(undefined) | ||||
|         let nextStep: UIEventSource<BaseUIElement> = new UIEventSource<BaseUIElement>(undefined) | ||||
|         const backButtonForNextStep = new SubtleButton(Svg.back_svg(), t.back).onClick(() => { | ||||
|             currentStepActive.setData(true) | ||||
|         }) | ||||
|          | ||||
|         let elements : (BaseUIElement | string)[] = [] | ||||
|         if(initial !== undefined){ | ||||
| 
 | ||||
|         let elements: (BaseUIElement | string)[] = [] | ||||
|         if (initial !== undefined) { | ||||
|             // Startup the flow
 | ||||
|             elements = [ | ||||
|                 initial, | ||||
|                 new Combine([ | ||||
|                     backbutton, | ||||
|                     new Toggle( | ||||
|                         new SubtleButton(Svg.back_svg().SetStyle("transform: rotate(180deg);"), t.next).onClick(() => { | ||||
|                         new SubtleButton( | ||||
|                             isConfirm ? Svg.checkmark_svg() : | ||||
|                                 Svg.back_svg().SetStyle("transform: rotate(180deg);"), | ||||
|                             isConfirm ? t.confirm : t.next | ||||
|                         ).onClick(() => { | ||||
|                             const v = initial.Value.data; | ||||
|                             nextStep.setData(constructNextstep(v, backButtonForNextStep)) | ||||
|                             currentStepActive.setData(false) | ||||
|  | @ -88,18 +115,18 @@ export class FlowPanel<T> extends Toggle { | |||
|                         initial.IsValid | ||||
|                     ) | ||||
|                 ]).SetClass("flex w-full justify-end space-x-2") | ||||
|                | ||||
| 
 | ||||
|             ] | ||||
|         } | ||||
|          | ||||
|          | ||||
| 
 | ||||
| 
 | ||||
|         super( | ||||
|             new Combine(elements).SetClass("h-full flex flex-col justify-between"), | ||||
|             new VariableUiElement(nextStep), | ||||
|             currentStepActive | ||||
|         ); | ||||
|         this.isActive = currentStepActive | ||||
|     } | ||||
|      | ||||
| 
 | ||||
|      | ||||
| 
 | ||||
| } | ||||
|  | @ -9,11 +9,18 @@ import MoreScreen from "../BigComponents/MoreScreen"; | |||
| import MinimapImplementation from "../Base/MinimapImplementation"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import {FlowPanel, FlowPanelFactory} from "./FlowStep"; | ||||
| import {FlowPanelFactory} from "./FlowStep"; | ||||
| import {RequestFile} from "./RequestFile"; | ||||
| import {DataPanel} from "./DataPanel"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import ConflationChecker from "./ConflationChecker"; | ||||
| import {AskMetadata} from "./AskMetadata"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import {ConfirmProcess} from "./ConfirmProcess"; | ||||
| import {CreateNotes} from "./CreateNotes"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import List from "../Base/List"; | ||||
| import {CompareToAlreadyExistingNotes} from "./CompareToAlreadyExistingNotes"; | ||||
| 
 | ||||
| export default class ImportHelperGui extends LoginToggle { | ||||
|     constructor() { | ||||
|  | @ -24,28 +31,51 @@ export default class ImportHelperGui extends LoginToggle { | |||
|         // We disable the userbadge, as various 'showData'-layers will give a read-only view in this case
 | ||||
|         state.featureSwitchUserbadge.setData(false) | ||||
| 
 | ||||
|         const {flow, furthestStep, titles} = | ||||
|             FlowPanelFactory | ||||
|                 .start("Select file", new RequestFile()) | ||||
|                 .then("Inspect data", geojson => new DataPanel(state, geojson)) | ||||
|                 .then("Compare with open notes", v => new CompareToAlreadyExistingNotes(state, v)) | ||||
|                 .then("Compare with existing data", v => new ConflationChecker(state, v)) | ||||
|                 .then("License and community check", v => new ConfirmProcess(v)) | ||||
|                 .then("Metadata", (v:{features:any[], layer: LayerConfig}) => new AskMetadata(v)) | ||||
|                 .finish("Note creation", v => new CreateNotes(state, v)); | ||||
|          | ||||
|         const toc = new List( | ||||
|             titles.map((title, i) => new VariableUiElement(furthestStep.map(currentStep => { | ||||
|                 if(i > currentStep){ | ||||
|                     return new Combine([title]).SetClass("subtle"); | ||||
|                 } | ||||
|                 if(i == currentStep){ | ||||
|                     return new Combine([title]).SetClass("font-bold"); | ||||
|                 } | ||||
|                 if(i < currentStep){ | ||||
|                     return title | ||||
|                 } | ||||
|                  | ||||
|                  | ||||
|             }))) | ||||
|             , true) | ||||
|          | ||||
|         const leftContents: BaseUIElement[] = [ | ||||
|             new BackToIndex().SetClass("block pl-4"), | ||||
|             toc, | ||||
|             new Toggle(new FixedUiElement("Testmode - won't actually import notes").SetClass("alert"), undefined, state.featureSwitchIsTesting), | ||||
|             LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"), | ||||
|         ].map(el => el?.SetClass("pl-4")) | ||||
| 
 | ||||
|         const leftBar = new Combine([ | ||||
|             new Combine(leftContents).SetClass("sticky top-4 m-4") | ||||
|         ]).SetClass("block w-full md:w-2/6 lg:w-1/6") | ||||
|             new Combine(leftContents).SetClass("sticky top-4 m-4"), | ||||
|           ]).SetClass("block w-full md:w-2/6 lg:w-1/6") | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|         const mainPanel =  | ||||
|             FlowPanelFactory | ||||
|                 .start(new RequestFile()) | ||||
|                 .then("datapanel", geojson => new DataPanel(state, geojson)) | ||||
|                 .then("conflation", v => new ConflationChecker(state, v)) | ||||
|                 .finish(_ => new FixedUiElement("All done!")) | ||||
|          | ||||
|         super( | ||||
|             new Toggle( | ||||
|                 new Combine([ | ||||
|                     leftBar, | ||||
|                     mainPanel.SetClass("m-8 w-full mb-24") | ||||
|                     flow.SetClass("m-8 w-full mb-24") | ||||
|                 ]).SetClass("h-full block md:flex") | ||||
| 
 | ||||
|                 , | ||||
|  |  | |||
							
								
								
									
										28
									
								
								UI/ImportFlow/ImportUtils.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								UI/ImportFlow/ImportUtils.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| 
 | ||||
| export class ImportUtils { | ||||
|     public static partitionFeaturesIfNearby(toPartitionFeatureCollection: ({ features: any[] }), compareWith: UIEventSource<{ features: any[] }>, cutoffDistanceInMeters: UIEventSource<number>): UIEventSource<{ hasNearby: any[], noNearby: any[] }> { | ||||
|         return compareWith.map(osmData => { | ||||
|             if (osmData?.features === undefined) { | ||||
|                 return undefined | ||||
|             } | ||||
|             const maxDist = cutoffDistanceInMeters.data | ||||
| 
 | ||||
| 
 | ||||
|             const hasNearby = [] | ||||
|             const noNearby = [] | ||||
|             for (const toImportElement of toPartitionFeatureCollection.features) { | ||||
|                 const hasNearbyFeature = osmData.features.some(f => | ||||
|                     maxDist >= GeoOperations.distanceBetween(toImportElement.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) | ||||
|                 if (hasNearbyFeature) { | ||||
|                     hasNearby.push(toImportElement) | ||||
|                 } else { | ||||
|                     noNearby.push(toImportElement) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return {hasNearby, noNearby} | ||||
|         }, [cutoffDistanceInMeters]); | ||||
|     } | ||||
| } | ||||
|  | @ -504,7 +504,8 @@ export default class ValidatedTextField { | |||
|         mapBackgroundLayer?: UIEventSource<any>, | ||||
|         unit?: Unit, | ||||
|         args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
 | ||||
|         feature?: any | ||||
|         feature?: any, | ||||
|         inputStyle?: string | ||||
|     }): InputElement<string> { | ||||
|         options = options ?? {}; | ||||
|         options.placeholder = options.placeholder ?? type; | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
| 
 | ||||
|     constructor( | ||||
|         state: { | ||||
|             featureSwitchIsTesting: UIEventSource<boolean>; | ||||
|             osmConnection: OsmConnection, | ||||
|             featurePipeline: FeaturePipeline, | ||||
|             backgroundLayer?: UIEventSource<BaseLayer> | ||||
|  | @ -167,8 +168,11 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
|         ).onClick(cancel) | ||||
| 
 | ||||
|         super([ | ||||
|             state.osmConnection.userDetails.data.dryRun ? | ||||
|                 Translations.t.general.testing.Clone().SetClass("alert") : undefined, | ||||
|             new Toggle( | ||||
|                 Translations.t.general.testing.SetClass("alert"), | ||||
|                 undefined, | ||||
|                 state.featureSwitchIsTesting | ||||
|             ), | ||||
|             disableFiltersOrConfirm, | ||||
|             cancelButton, | ||||
|             preset.description, | ||||
|  |  | |||
|  | @ -141,7 +141,7 @@ ${Utils.special_visualizations_importRequirementDocs} | |||
|         if(tagSpec.indexOf(" ")< 0 && tagSpec.indexOf(";") < 0 && tagSource.data[args.tags] !== undefined){ | ||||
|             // This is probably a key
 | ||||
|             tagSpec = tagSource.data[args.tags] | ||||
|             console.warn("Using tagspec tagSource.data["+args.tags+"] which is ",tagSpec) | ||||
|             console.debug("The import button is using tags from properties["+args.tags+"] of this object, namely ",tagSpec) | ||||
|         } | ||||
| 
 | ||||
|         const importClicked = new UIEventSource(false); | ||||
|  | @ -201,7 +201,7 @@ ${Utils.special_visualizations_importRequirementDocs} | |||
|             if(tags.indexOf(" ") < 0 && tags.indexOf(";") < 0 && originalFeatureTags.data[tags] !== undefined){ | ||||
|                 // This might be a property to expand...
 | ||||
|                 const items : string = originalFeatureTags.data[tags] | ||||
|                 console.warn("Using tagspec tagSource.data["+tags+"] which is ",items) | ||||
|                 console.debug("The import button is using tags from properties["+tags+"] of this object, namely ",items) | ||||
|                 baseArgs["newTags"] = TagApplyButton.generateTagsToApply(items, originalFeatureTags) | ||||
|             }else{ | ||||
|                 baseArgs["newTags"] = TagApplyButton.generateTagsToApply(tags, originalFeatureTags) | ||||
|  |  | |||
|  | @ -55,6 +55,45 @@ export interface SpecialVisualization { | |||
|     getLayerDependencies?: (argument: string[]) => string[] | ||||
| } | ||||
| 
 | ||||
| export class AllTagsPanel extends VariableUiElement { | ||||
| 
 | ||||
|     constructor(tags: UIEventSource<any>, state?) { | ||||
|          | ||||
|         const calculatedTags = [].concat( | ||||
|             SimpleMetaTagger.lazyTags, | ||||
|             ...(state?.layoutToUse?.layers?.map(l => l.calculatedTags?.map(c => c[0]) ?? []) ?? [])) | ||||
|          | ||||
|          | ||||
|         super(tags.map(tags => { | ||||
|             const parts = []; | ||||
|             for (const key in tags) { | ||||
|                 if (!tags.hasOwnProperty(key)) { | ||||
|                     continue | ||||
|                 } | ||||
|                 let v = tags[key] | ||||
|                 if (v === "") { | ||||
|                     v = "<b>empty string</b>" | ||||
|                 } | ||||
|                 parts.push([key, v ?? "<b>undefined</b>"]); | ||||
|             } | ||||
|      | ||||
|             for (const key of calculatedTags) { | ||||
|                 const value = tags[key] | ||||
|                 if (value === undefined) { | ||||
|                     continue | ||||
|                 } | ||||
|                 parts.push(["<i>" + key + "</i>", value]) | ||||
|             } | ||||
|      | ||||
|             return new Table( | ||||
|                 ["key", "value"], | ||||
|                 parts | ||||
|             ) | ||||
|             .SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;").SetClass("zebra-table") | ||||
|         })) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default class SpecialVisualizations { | ||||
| 
 | ||||
|     public static specialVisualizations = SpecialVisualizations.init() | ||||
|  | @ -99,37 +138,7 @@ export default class SpecialVisualizations { | |||
|                     funcName: "all_tags", | ||||
|                     docs: "Prints all key-value pairs of the object - used for debugging", | ||||
|                     args: [], | ||||
|                     constr: ((state, tags: UIEventSource<any>) => { | ||||
|                         const calculatedTags = [].concat( | ||||
|                             SimpleMetaTagger.lazyTags, | ||||
|                             ...(state?.layoutToUse?.layers?.map(l => l.calculatedTags?.map(c => c[0]) ?? []) ?? [])) | ||||
|                         return new VariableUiElement(tags.map(tags => { | ||||
|                             const parts = []; | ||||
|                             for (const key in tags) { | ||||
|                                 if (!tags.hasOwnProperty(key)) { | ||||
|                                     continue | ||||
|                                 } | ||||
|                                 let v = tags[key] | ||||
|                                 if (v === "") { | ||||
|                                     v = "<b>empty string</b>" | ||||
|                                 } | ||||
|                                 parts.push([key, v ?? "<b>undefined</b>"]); | ||||
|                             } | ||||
| 
 | ||||
|                             for (const key of calculatedTags) { | ||||
|                                 const value = tags[key] | ||||
|                                 if (value === undefined) { | ||||
|                                     continue | ||||
|                                 } | ||||
|                                 parts.push(["<i>" + key + "</i>", value]) | ||||
|                             } | ||||
| 
 | ||||
|                             return new Table( | ||||
|                                 ["key", "value"], | ||||
|                                 parts | ||||
|                             ) | ||||
|                         })).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;").SetClass("zebra-table") | ||||
|                     }) | ||||
|                     constr: ((state, tags: UIEventSource<any>) => new AllTagsPanel(tags, state)) | ||||
|                 }, | ||||
|                 { | ||||
|                     funcName: "image_carousel", | ||||
|  | @ -339,7 +348,7 @@ export default class SpecialVisualizations { | |||
|                         const mangrove = MangroveReviews.Get(Number(tgs._lon), Number(tgs._lat), | ||||
|                             encodeURIComponent(subject), | ||||
|                             state.mangroveIdentity, | ||||
|                             state.osmConnection._dryRun | ||||
|                             state.featureSwitchIsTesting.data | ||||
|                         ); | ||||
|                         const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection); | ||||
|                         return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form); | ||||
|  | @ -743,10 +752,6 @@ export default class SpecialVisualizations { | |||
|                             return t.addCommentAndClose | ||||
|                         }))).onClick(() => { | ||||
|                             const id = tags.data[args[1] ?? "id"] | ||||
|                             if (state.featureSwitchIsTesting.data) { | ||||
|                                 console.log("Testmode: Not actually closing note...") | ||||
|                                 return; | ||||
|                             } | ||||
|                             state.osmConnection.closeNote(id, txt.data).then(_ => { | ||||
|                                 tags.data["closed_at"] = new Date().toISOString(); | ||||
|                                 tags.ping() | ||||
|  | @ -760,10 +765,6 @@ export default class SpecialVisualizations { | |||
|                             return t.reopenNoteAndComment | ||||
|                         }))).onClick(() => { | ||||
|                             const id = tags.data[args[1] ?? "id"] | ||||
|                             if (state.featureSwitchIsTesting.data) { | ||||
|                                 console.log("Testmode: Not actually reopening note...") | ||||
|                                 return; | ||||
|                             } | ||||
|                             state.osmConnection.reopenNote(id, txt.data).then(_ => { | ||||
|                                 tags.data["closed_at"] = undefined; | ||||
|                                 tags.ping() | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
|   "description": "This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)", | ||||
|   "source": { | ||||
|     "osmTags": "id~*", | ||||
|     "geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=7&bbox={x_min},{y_min},{x_max},{y_max}", | ||||
|     "geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=7&bbox={x_min},{y_min},{x_max},{y_max}", | ||||
|     "geoJsonZoomLevel": 12, | ||||
|     "maxCacheAge": 0 | ||||
|   }, | ||||
|  | @ -29,7 +29,8 @@ | |||
|     "_opened_by_anonymous_user:=feat.get('comments')[0].user === undefined", | ||||
|     "_first_user:=feat.get('comments')[0].user", | ||||
|     "_first_user_lc:=feat.get('comments')[0].user?.toLowerCase()", | ||||
|     "_first_user_id:=feat.get('comments')[0].uid" | ||||
|     "_first_user_id:=feat.get('comments')[0].uid", | ||||
|     "_is_import_note:=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\).*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()" | ||||
|   ], | ||||
|   "titleIcons": [ | ||||
|     { | ||||
|  | @ -201,6 +202,17 @@ | |||
|           } | ||||
|         } | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "id": "no_imports", | ||||
|       "options": [ | ||||
|         { | ||||
|           "osmTags": "_is_import_note=", | ||||
|           "question": { | ||||
|             "en": "Hide import notes" | ||||
|           } | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -1493,6 +1493,10 @@ video { | |||
|   padding: 0.125rem; | ||||
| } | ||||
| 
 | ||||
| .p-8 { | ||||
|   padding: 2rem; | ||||
| } | ||||
| 
 | ||||
| .pb-12 { | ||||
|   padding-bottom: 3rem; | ||||
| } | ||||
|  |  | |||
|  | @ -465,7 +465,9 @@ | |||
|     "importLayer": { | ||||
|         "layerName": "Possible {title}", | ||||
|         "description": "A layer which imports entries for {title}", | ||||
|         "popupTitle": "Possible {title}" | ||||
|         "popupTitle": "Possible {title}", | ||||
|         "importButton": "import_button({layerId}, _tags, There might be a {title} here,./assets/svg/addSmall.svg,,,id)", | ||||
|         "importHandled": "<div class='thanks'>This feature has been handled! Thanks for your effort</div>" | ||||
|     }, | ||||
|     "importHelper": { | ||||
|         "title": "Import helper", | ||||
|  | @ -477,5 +479,5 @@ | |||
|         "selectLayer": "Select a layer...", | ||||
|         "selectFileTitle": "Select file", | ||||
|         "validateDataTitle": "Validate data" | ||||
|     } | ||||
|      } | ||||
| } | ||||
|  |  | |||
|  | @ -3354,6 +3354,13 @@ | |||
|                         "question": "Only show open notes" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "8": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Hide import notes" | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "name": "OpenStreetMap notes", | ||||
|  |  | |||
|  | @ -314,5 +314,12 @@ | |||
|     }, | ||||
|     "multi_apply": { | ||||
|         "autoApply": "Wijzigingen aan eigenschappen {attr_names} zullen ook worden uitgevoerd op {count} andere objecten." | ||||
|     }, | ||||
|     "importLayer": { | ||||
|         "layerName": "Hier is misschien een {title}", | ||||
|         "description": "Deze laag toont kaart-nota's die wijzen op een {title}", | ||||
|         "popupTitle": "Mogelijkse {title}", | ||||
|         "importButton": "import_button({layerId}, _tags, Hier is een {title}, voeg toe...,./assets/svg/addSmall.svg,,,id)", | ||||
|         "importHandled": "<div class='thanks'>Dit punt is afgehandeld. Bedankt om mee te helpen!</div>" | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -5,9 +5,6 @@ import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; | |||
| import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | ||||
| import Constants from "../Models/Constants"; | ||||
| import { | ||||
|     DesugaringContext, | ||||
|     PrepareLayer, | ||||
|     PrepareTheme, | ||||
|     ValidateLayer, | ||||
|     ValidateThemeAndLayers | ||||
| } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; | ||||
|  | @ -16,6 +13,9 @@ import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingCon | |||
| import * as questions from "../assets/tagRenderings/questions.json"; | ||||
| import * as icons from "../assets/tagRenderings/icons.json"; | ||||
| import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson"; | ||||
| import {PrepareLayer} from "../Models/ThemeConfig/Conversion/PrepareLayer"; | ||||
| import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme"; | ||||
| import {DesugaringContext} from "../Models/ThemeConfig/Conversion/Conversion"; | ||||
| 
 | ||||
| // This scripts scans 'assets/layers/*.json' for layer definition files and 'assets/themes/*.json' for theme definition files.
 | ||||
| // It spits out an overview of those to be used to load them
 | ||||
|  |  | |||
|  | @ -1,10 +1,11 @@ | |||
| import T from "./TestHelper"; | ||||
| import CreateNoteImportLayer from "../Models/ThemeConfig/Conversion/CreateNoteImportLayer"; | ||||
| import * as bookcases from "../assets/layers/public_bookcase/public_bookcase.json" | ||||
| import {DesugaringContext, PrepareLayer} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; | ||||
| import {DesugaringContext} from "../Models/ThemeConfig/Conversion/Conversion"; | ||||
| import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | ||||
| import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| import {PrepareLayer} from "../Models/ThemeConfig/Conversion/PrepareLayer"; | ||||
| 
 | ||||
| export default class CreateNoteImportLayerSpec extends T { | ||||
| 
 | ||||
|  | @ -17,7 +18,7 @@ export default class CreateNoteImportLayerSpec extends T { | |||
| 
 | ||||
|                 } | ||||
|                 const layerPrepare = new PrepareLayer() | ||||
|                 const layer = new LayerConfig(layerPrepare.convertStrict(desugaringState, bookcases, "ImportLayerGeneratorTest:Parse bookcases"), "ImportLayerGeneratorTest: init bookcases-layer") | ||||
|                 const layer =layerPrepare.convertStrict(desugaringState, bookcases, "ImportLayerGeneratorTest:Parse bookcases") | ||||
|                 const generator = new CreateNoteImportLayer() | ||||
|                 const generatedLayer = generator.convertStrict(desugaringState, layer, "ImportLayerGeneratorTest: convert") | ||||
|        //         fs.writeFileSync("bookcases-import-layer.generated.json", JSON.stringify(generatedLayer, null, "  "), "utf8")
 | ||||
|  |  | |||
|  | @ -112,6 +112,12 @@ export default class TagSpec extends T { | |||
|                 equal(compare.matchesProperties({"key": "5"}), true); | ||||
|                 equal(compare.matchesProperties({"key": "4.2"}), false); | ||||
| 
 | ||||
|                 const importMatch = TagUtils.Tag("tags~(^|.*;)amenity=public_bookcase($|;.*)") | ||||
|                 equal(importMatch.matchesProperties({"tags": "amenity=public_bookcase;name=test"}), true) | ||||
|                 equal(importMatch.matchesProperties({"tags": "amenity=public_bookcase"}), true) | ||||
|                 equal(importMatch.matchesProperties({"tags": "name=test;amenity=public_bookcase"}), true) | ||||
|                 equal(importMatch.matchesProperties({"tags": "amenity=bench"}), false) | ||||
| 
 | ||||
|             })], | ||||
|             ["Is equivalent test", (() => { | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ import * as assert from "assert"; | |||
| import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||
| import * as bookcaseLayer from "../assets/generated/layers/public_bookcase.json" | ||||
| import {PrepareTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; | ||||
| import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; | ||||
| import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | ||||
| import Constants from "../Models/Constants"; | ||||
| import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme"; | ||||
| 
 | ||||
| export default class ThemeSpec extends T { | ||||
|     constructor() { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue