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 {LocalStorageSource} from "./Web/LocalStorageSource"; | ||||||
| import LZString from "lz-string"; | import LZString from "lz-string"; | ||||||
| import * as personal from "../assets/themes/personal/personal.json"; | 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 {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | ||||||
| import SharedTagRenderings from "../Customizations/SharedTagRenderings"; | import SharedTagRenderings from "../Customizations/SharedTagRenderings"; | ||||||
| import * as known_layers from "../assets/generated/known_layers.json" | import * as known_layers from "../assets/generated/known_layers.json" | ||||||
| import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; | import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; | ||||||
|  | import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme"; | ||||||
| 
 | 
 | ||||||
| export default class DetermineLayout { | export default class DetermineLayout { | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -29,11 +29,11 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti | ||||||
|         }, |         }, | ||||||
|         tileIndex, |         tileIndex, | ||||||
|         upstream: FeatureSourceForLayer, |         upstream: FeatureSourceForLayer, | ||||||
|         metataggingUpdated: UIEventSource<any> |         metataggingUpdated?: UIEventSource<any> | ||||||
|     ) { |     ) { | ||||||
|         this.name = "FilteringFeatureSource(" + upstream.name + ")" |         this.name = "FilteringFeatureSource(" + upstream.name + ")" | ||||||
|         this.tileIndex = tileIndex |         this.tileIndex = tileIndex | ||||||
|         this.bbox = BBox.fromTileIndex(tileIndex) |         this.bbox = tileIndex === undefined ? undefined : BBox.fromTileIndex(tileIndex) | ||||||
|         this.upstream = upstream |         this.upstream = upstream | ||||||
|         this.state = state |         this.state = state | ||||||
| 
 | 
 | ||||||
|  | @ -55,7 +55,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|          |          | ||||||
|         metataggingUpdated.addCallback(_ => { |         metataggingUpdated?.addCallback(_ => { | ||||||
|             self._is_dirty.setData(true) |             self._is_dirty.setData(true) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  | @ -63,6 +63,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private update() { |     private update() { | ||||||
|  |         console.log("FIltering", this.upstream.name) | ||||||
|         const self = this; |         const self = this; | ||||||
|         const layer = this.upstream.layer; |         const layer = this.upstream.layer; | ||||||
|         const features: { feature: any; freshness: Date }[] = (this.upstream.features.data ?? []); |         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>> |     private readonly featureIdBlacklist?: UIEventSource<Set<string>> | ||||||
| 
 | 
 | ||||||
|     public constructor(flayer: FilteredLayer, |     public constructor(flayer: FilteredLayer, | ||||||
|                        zxy?: [number, number, number], |                        zxy?: [number, number, number] | BBox, | ||||||
|                        options?: { |                        options?: { | ||||||
|                            featureIdBlacklist?: UIEventSource<Set<string>> |                            featureIdBlacklist?: UIEventSource<Set<string>> | ||||||
|                        }) { |                        }) { | ||||||
|  | @ -41,23 +41,32 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | ||||||
|         this.featureIdBlacklist = options?.featureIdBlacklist |         this.featureIdBlacklist = options?.featureIdBlacklist | ||||||
|         let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); |         let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); | ||||||
|         if (zxy !== undefined) { |         if (zxy !== undefined) { | ||||||
|  |             let tile_bbox: BBox; | ||||||
|  |             if (zxy instanceof BBox) { | ||||||
|  |                 tile_bbox = zxy; | ||||||
|  |             } else { | ||||||
|                 const [z, x, y] = zxy; |                 const [z, x, y] = zxy; | ||||||
|             let tile_bbox = BBox.fromTile(z, x, y) |                 tile_bbox = BBox.fromTile(z, x, y); | ||||||
|             let bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox | 
 | ||||||
|             if (this.layer.layerDef.source.mercatorCrs) { |                 this.tileIndex = Tiles.tile_index(z, x, y) | ||||||
|                 bounds = tile_bbox.toMercator() |                 this.bbox = BBox.fromTile(z, x, y) | ||||||
|             } |  | ||||||
|                 url = url |                 url = url | ||||||
|                     .replace('{z}', "" + z) |                     .replace('{z}', "" + z) | ||||||
|                     .replace('{x}', "" + x) |                     .replace('{x}', "" + x) | ||||||
|                     .replace('{y}', "" + y) |                     .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('{y_min}', "" + bounds.minLat) |                 .replace('{y_min}', "" + bounds.minLat) | ||||||
|                 .replace('{y_max}', "" + bounds.maxLat) |                 .replace('{y_max}', "" + bounds.maxLat) | ||||||
|                 .replace('{x_min}', "" + bounds.minLon) |                 .replace('{x_min}', "" + bounds.minLon) | ||||||
|                 .replace('{x_max}', "" + bounds.maxLon) |                 .replace('{x_max}', "" + bounds.maxLon) | ||||||
| 
 | 
 | ||||||
|             this.tileIndex = Tiles.tile_index(z, x, y) | 
 | ||||||
|             this.bbox = BBox.fromTile(z, x, y) |  | ||||||
|         } else { |         } else { | ||||||
|             this.tileIndex = Tiles.tile_index(0, 0, 0) |             this.tileIndex = Tiles.tile_index(0, 0, 0) | ||||||
|             this.bbox = BBox.global; |             this.bbox = BBox.global; | ||||||
|  |  | ||||||
|  | @ -683,6 +683,8 @@ export class GeoOperations { | ||||||
|         throw "CalculateIntersection fallthrough: can not calculate an intersection between features" |         throw "CalculateIntersection fallthrough: can not calculate an intersection between features" | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |      | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -18,13 +18,13 @@ export class ChangesetHandler { | ||||||
|     private readonly allElements: ElementStorage; |     private readonly allElements: ElementStorage; | ||||||
|     private osmConnection: OsmConnection; |     private osmConnection: OsmConnection; | ||||||
|     private readonly changes: Changes; |     private readonly changes: Changes; | ||||||
|     private readonly _dryRun: boolean; |     private readonly _dryRun: UIEventSource<boolean>; | ||||||
|     private readonly userDetails: UIEventSource<UserDetails>; |     private readonly userDetails: UIEventSource<UserDetails>; | ||||||
|     private readonly auth: any; |     private readonly auth: any; | ||||||
|     private readonly backend: string; |     private readonly backend: string; | ||||||
| 
 | 
 | ||||||
|     constructor(layoutName: string, |     constructor(layoutName: string, | ||||||
|                 dryRun: boolean, |                 dryRun: UIEventSource<boolean>, | ||||||
|                 osmConnection: OsmConnection, |                 osmConnection: OsmConnection, | ||||||
|                 allElements: ElementStorage, |                 allElements: ElementStorage, | ||||||
|                 changes: Changes, |                 changes: Changes, | ||||||
|  | @ -67,7 +67,7 @@ export class ChangesetHandler { | ||||||
|             this.userDetails.data.csCount = 1; |             this.userDetails.data.csCount = 1; | ||||||
|             this.userDetails.ping(); |             this.userDetails.ping(); | ||||||
|         } |         } | ||||||
|         if (this._dryRun) { |         if (this._dryRun.data) { | ||||||
|             const changesetXML = generateChangeXML(123456); |             const changesetXML = generateChangeXML(123456); | ||||||
|             console.log("Metatags are", extraMetaTags) |             console.log("Metatags are", extraMetaTags) | ||||||
|             console.log(changesetXML); |             console.log(changesetXML); | ||||||
|  |  | ||||||
|  | @ -19,7 +19,6 @@ export default class UserDetails { | ||||||
|     public img: string; |     public img: string; | ||||||
|     public unreadMessages = 0; |     public unreadMessages = 0; | ||||||
|     public totalMessages = 0; |     public totalMessages = 0; | ||||||
|     public dryRun: boolean; |  | ||||||
|     home: { lon: number; lat: number }; |     home: { lon: number; lat: number }; | ||||||
|     public backend: string; |     public backend: string; | ||||||
| 
 | 
 | ||||||
|  | @ -47,7 +46,6 @@ export class OsmConnection { | ||||||
|     public auth; |     public auth; | ||||||
|     public userDetails: UIEventSource<UserDetails>; |     public userDetails: UIEventSource<UserDetails>; | ||||||
|     public isLoggedIn: UIEventSource<boolean> |     public isLoggedIn: UIEventSource<boolean> | ||||||
|     _dryRun: boolean; |  | ||||||
|     public preferencesHandler: OsmPreferences; |     public preferencesHandler: OsmPreferences; | ||||||
|     public changesetHandler: ChangesetHandler; |     public changesetHandler: ChangesetHandler; | ||||||
|     public readonly _oauth_config: { |     public readonly _oauth_config: { | ||||||
|  | @ -55,6 +53,7 @@ export class OsmConnection { | ||||||
|         oauth_secret: string, |         oauth_secret: string, | ||||||
|         url: string |         url: string | ||||||
|     }; |     }; | ||||||
|  |     private readonly _dryRun: UIEventSource<boolean>; | ||||||
|     private fakeUser: boolean; |     private fakeUser: boolean; | ||||||
|     private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []; |     private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []; | ||||||
|     private readonly _iframeMode: Boolean | boolean; |     private readonly _iframeMode: Boolean | boolean; | ||||||
|  | @ -62,7 +61,7 @@ export class OsmConnection { | ||||||
|     private isChecking = false; |     private isChecking = false; | ||||||
| 
 | 
 | ||||||
|     constructor(options: { |     constructor(options: { | ||||||
|                     dryRun?: false | boolean, |                     dryRun?: UIEventSource<boolean>, | ||||||
|                     fakeUser?: false | boolean, |                     fakeUser?: false | boolean, | ||||||
|                     allElements: ElementStorage, |                     allElements: ElementStorage, | ||||||
|                     changes: Changes, |                     changes: Changes, | ||||||
|  | @ -82,7 +81,6 @@ export class OsmConnection { | ||||||
|         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; |         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; | ||||||
| 
 | 
 | ||||||
|         this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails"); |         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) { |         if (options.fakeUser) { | ||||||
|             const ud = this.userDetails.data; |             const ud = this.userDetails.data; | ||||||
|             ud.csCount = 5678 |             ud.csCount = 5678 | ||||||
|  | @ -99,13 +97,13 @@ export class OsmConnection { | ||||||
|                 self.AttemptLogin() |                 self.AttemptLogin() | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|         this._dryRun = options.dryRun; |         this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false); | ||||||
| 
 | 
 | ||||||
|         this.updateAuthObject(); |         this.updateAuthObject(); | ||||||
| 
 | 
 | ||||||
|         this.preferencesHandler = new OsmPreferences(this.auth, this); |         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) { |         if (options.oauth_token?.data !== undefined) { | ||||||
|             console.log(options.oauth_token.data) |             console.log(options.oauth_token.data) | ||||||
|             const self = this; |             const self = this; | ||||||
|  | @ -223,7 +221,7 @@ export class OsmConnection { | ||||||
|         if ((text ?? "") !== "") { |         if ((text ?? "") !== "") { | ||||||
|             textSuffix = "?text=" + encodeURIComponent(text) |             textSuffix = "?text=" + encodeURIComponent(text) | ||||||
|         } |         } | ||||||
|         if (this._dryRun) { |         if (this._dryRun.data) { | ||||||
|             console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) |             console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) | ||||||
|             return new Promise((ok, error) => { |             return new Promise((ok, error) => { | ||||||
|                 ok() |                 ok() | ||||||
|  | @ -246,7 +244,7 @@ export class OsmConnection { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public reopenNote(id: number | string, text?: string): Promise<any> { |     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) |             console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) | ||||||
|             return new Promise((ok, error) => { |             return new Promise((ok, error) => { | ||||||
|                 ok() |                 ok() | ||||||
|  | @ -273,10 +271,10 @@ export class OsmConnection { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { |     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) |             console.warn("Dryrun enabled - not actually opening note with text ", text) | ||||||
|             return new Promise((ok, error) => { |             return new Promise<{ id: number }>((ok, error) => { | ||||||
|                 ok() |                 window.setTimeout(() => ok({id: Math.floor(Math.random() * 1000)}), Math.random() * 5000) | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|         const auth = this.auth; |         const auth = this.auth; | ||||||
|  | @ -285,15 +283,18 @@ export class OsmConnection { | ||||||
|             auth.xhr({ |             auth.xhr({ | ||||||
|                 method: 'POST', |                 method: 'POST', | ||||||
|                 path: `/api/0.6/notes.json`, |                 path: `/api/0.6/notes.json`, | ||||||
|                 options: {header: |                 options: { | ||||||
|                         {'Content-Type': 'application/json'}}, |                     header: | ||||||
|  |                         {'Content-Type': 'application/json'} | ||||||
|  |                 }, | ||||||
|                 content: JSON.stringify(content) |                 content: JSON.stringify(content) | ||||||
| 
 | 
 | ||||||
|             }, function (err, response) { |             }, function (err, response) { | ||||||
|                 if (err !== null) { |                 if (err !== null) { | ||||||
|                     error(err) |                     error(err) | ||||||
|                 } else { |                 } else { | ||||||
|                     const id = Number(response.children[0].children[0].children.item("id").innerHTML) | 
 | ||||||
|  |                     const id = response.properties.id | ||||||
|                     console.log("OPENED NOTE", id) |                     console.log("OPENED NOTE", id) | ||||||
|                     ok({id}) |                     ok({id}) | ||||||
|                 } |                 } | ||||||
|  | @ -304,7 +305,7 @@ export class OsmConnection { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public addCommentToNode(id: number | string, text: string): Promise<any> { |     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) |             console.warn("Dryrun enabled - not actually adding comment ", text, "to  note ", id) | ||||||
|             return new Promise((ok, error) => { |             return new Promise((ok, error) => { | ||||||
|                 ok() |                 ok() | ||||||
|  |  | ||||||
|  | @ -43,7 +43,7 @@ export default class UserRelatedState extends ElementsState { | ||||||
| 
 | 
 | ||||||
|         this.osmConnection = new OsmConnection({ |         this.osmConnection = new OsmConnection({ | ||||||
|             changes: this.changes, |             changes: this.changes, | ||||||
|             dryRun: this.featureSwitchIsTesting.data, |             dryRun: this.featureSwitchIsTesting, | ||||||
|             fakeUser: this.featureSwitchFakeUser.data, |             fakeUser: this.featureSwitchFakeUser.data, | ||||||
|             allElements: this.allElements, |             allElements: this.allElements, | ||||||
|             oauth_token: QueryParameters.GetQueryParameter( |             oauth_token: QueryParameters.GetQueryParameter( | ||||||
|  |  | ||||||
|  | @ -10,6 +10,13 @@ export class RegexTag extends TagsFilter { | ||||||
|     constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) { |     constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) { | ||||||
|         super(); |         super(); | ||||||
|         this.key = key; |         this.key = key; | ||||||
|  |         if (typeof value === "string") { | ||||||
|  |             if (value.indexOf("^") < 0 && value.indexOf("$") < 0) { | ||||||
|  |                 value = "^" + value + "$" | ||||||
|  |             } | ||||||
|  |             value = new RegExp(value) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         this.value = value; |         this.value = value; | ||||||
|         this.invert = invert; |         this.invert = invert; | ||||||
|         this.matchesEmpty = RegexTag.doesMatch("", this.value); |         this.matchesEmpty = RegexTag.doesMatch("", this.value); | ||||||
|  |  | ||||||
|  | @ -192,16 +192,16 @@ export class TagUtils { | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     const f = (value: string | undefined) => { |                     const f = (value: string | undefined) => { | ||||||
|                         if(value === undefined){ |                         if (value === undefined) { | ||||||
|                             return false; |                             return false; | ||||||
|                         } |                         } | ||||||
|                         let b = Number(value?.trim() ) |                         let b = Number(value?.trim()) | ||||||
|                         if (isNaN(b)) { |                         if (isNaN(b)) { | ||||||
|                             if(value.endsWith(" UTC")) { |                             if (value.endsWith(" UTC")) { | ||||||
|                                 value = value.replace(" UTC", "+00") |                                 value = value.replace(" UTC", "+00") | ||||||
|                             } |                             } | ||||||
|                             b = new Date(value).getTime() |                             b = new Date(value).getTime() | ||||||
|                             if(isNaN(b)){ |                             if (isNaN(b)) { | ||||||
|                                 return false |                                 return false | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|  | @ -218,7 +218,7 @@ export class TagUtils { | ||||||
|                 } |                 } | ||||||
|                 return new RegexTag( |                 return new RegexTag( | ||||||
|                     split[0], |                     split[0], | ||||||
|                     new RegExp("^" + split[1] + "$"), |                     split[1], | ||||||
|                     true |                     true | ||||||
|                 ); |                 ); | ||||||
|             } |             } | ||||||
|  | @ -228,8 +228,8 @@ export class TagUtils { | ||||||
|                     split[1] = "..*" |                     split[1] = "..*" | ||||||
|                 } |                 } | ||||||
|                 return new RegexTag( |                 return new RegexTag( | ||||||
|                     new RegExp("^" + split[0] + "$"), |                     split[0], | ||||||
|                     new RegExp("^" + split[1] + "$") |                     split[1] | ||||||
|                 ); |                 ); | ||||||
|             } |             } | ||||||
|             if (tag.indexOf("!:=") >= 0) { |             if (tag.indexOf("!:=") >= 0) { | ||||||
|  | @ -259,7 +259,7 @@ export class TagUtils { | ||||||
|                 } |                 } | ||||||
|                 return new RegexTag( |                 return new RegexTag( | ||||||
|                     split[0], |                     split[0], | ||||||
|                     new RegExp("^" + split[1] + "$"), |                     split[1], | ||||||
|                     true |                     true | ||||||
|                 ); |                 ); | ||||||
|             } |             } | ||||||
|  | @ -273,7 +273,7 @@ export class TagUtils { | ||||||
|                 } |                 } | ||||||
|                 return new RegexTag( |                 return new RegexTag( | ||||||
|                     split[0], |                     split[0], | ||||||
|                     new RegExp("^" + split[1] + "$") |                     split[1] | ||||||
|                 ); |                 ); | ||||||
|             } |             } | ||||||
|             if (tag.indexOf("=") >= 0) { |             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 LayerConfig from "../LayerConfig"; | ||||||
| import {LayerConfigJson} from "../Json/LayerConfigJson"; | import {LayerConfigJson} from "../Json/LayerConfigJson"; | ||||||
| import Translations from "../../../UI/i18n/Translations"; | import Translations from "../../../UI/i18n/Translations"; | ||||||
| import {TagsFilter} from "../../../Logic/Tags/TagsFilter"; | import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"; | ||||||
| import {And} from "../../../Logic/Tags/And"; |  | ||||||
| 
 | 
 | ||||||
| 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([ |         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').", |             "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", |             "The import buttons and matches will be based on the presets of the given theme", | ||||||
|         ].join("\n\n"), []) |         ].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 errors = [] | ||||||
|         const warnings = [] |         const warnings = [] | ||||||
|         const t = Translations.t.importLayer; |         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 = { |         const result : LayerConfigJson = { | ||||||
|             "id": "note_import_"+layer.id, |             "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, |             "description": t.description.Subs({title: layer.title.render}).translations, | ||||||
|             "source": { |             "source": { | ||||||
|                 "osmTags": { |                 "osmTags": { | ||||||
|  | @ -31,27 +71,32 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer | ||||||
|                         "id~*" |                         "id~*" | ||||||
|                     ] |                     ] | ||||||
|                 }, |                 }, | ||||||
|                 "geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=0&bbox={x_min},{y_min},{x_max},{y_max}", |                 "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": 12, |                 "geoJsonZoomLevel": 10, | ||||||
|                 "maxCacheAge": 0 |                 "maxCacheAge": 0 | ||||||
|             }, |             }, | ||||||
|             "minzoom": 10, |             "minzoom": 12, | ||||||
|             "title": { |             "title": { | ||||||
|                 "render": t.popupTitle.Subs({title: layer.presets[0].title}).translations |                 "render": t.popupTitle.Subs({title: layer.presets[0].title}).translations | ||||||
|             }, |             }, | ||||||
|             "calculatedTags": [ |             "calculatedTags": [ | ||||||
|                 "_first_comment:=feat.get('comments')[0].text.toLowerCase()", |                 "_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] })()", |                 "_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('');})()", |                 "_comments_count=feat.get('comments').length", | ||||||
|                 "_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(';');})()" |                 "_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": { |             "isShown": { | ||||||
|                 "render": "no", |                 "render": "no", | ||||||
|                 "mappings": [ |                 "mappings": [ | ||||||
|  |                     { | ||||||
|  |                         "if": "comments!~.*https://mapcomplete.osm.be.*", | ||||||
|  |                         "then":"no" | ||||||
|  |                     }, | ||||||
|                     { |                     { | ||||||
|                         "if": {and:  |                         "if": {and:  | ||||||
|                                 ["_trigger_index~*", |                                 ["_trigger_index~*", | ||||||
|                                     {or: possibleTags.map(tf => tf.AsJson())} |                                     {or: isShownIfAny} | ||||||
|                                 ]}, |                                 ]}, | ||||||
|                         "then": "yes" |                         "then": "yes" | ||||||
|                     } |                     } | ||||||
|  | @ -63,25 +108,34 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer | ||||||
|                 } |                 } | ||||||
|             ], |             ], | ||||||
|             "tagRenderings": [ |             "tagRenderings": [ | ||||||
|                 { |  | ||||||
|                     "id": "conversation", |  | ||||||
|                     "render": "{visualize_note_comments(comments,1)}" |  | ||||||
|                 }, |  | ||||||
|                 { |                 { | ||||||
|                     "id": "Intro", |                     "id": "Intro", | ||||||
|                     "render": "{_intro}" |                     "render": "{_intro}" | ||||||
|                 }, |                 }, | ||||||
|  |                 { | ||||||
|  |                     "id": "conversation", | ||||||
|  |                     "render": "{visualize_note_comments(comments,1)}", | ||||||
|  |                     condition: "_comments_count>1" | ||||||
|  |                 }, | ||||||
|                 { |                 { | ||||||
|                     "id": "import", |                     "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_", |                     "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", |                     "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", |                     "id": "comment", | ||||||
|  | @ -90,6 +144,10 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer | ||||||
|                 { |                 { | ||||||
|                     "id": "add_image", |                     "id": "add_image", | ||||||
|                     "render": "{add_image_to_note()}" |                     "render": "{add_image_to_note()}" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     id:"alltags", | ||||||
|  |                     render:"{all_tags()}" | ||||||
|                 } |                 } | ||||||
|             ], |             ], | ||||||
|             "mapRendering": [ |             "mapRendering": [ | ||||||
|  | @ -99,9 +157,14 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer | ||||||
|                         "centroid" |                         "centroid" | ||||||
|                     ], |                     ], | ||||||
|                     "icon": { |                     "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 {LayoutConfigJson} from "../Json/LayoutConfigJson"; | ||||||
| import DependencyCalculator from "../DependencyCalculator"; |  | ||||||
| import LayerConfig from "../LayerConfig"; | import LayerConfig from "../LayerConfig"; | ||||||
| import {Translation} from "../../../UI/i18n/Translation"; | import {Translation} from "../../../UI/i18n/Translation"; | ||||||
| import LayoutConfig from "../LayoutConfig"; | import LayoutConfig from "../LayoutConfig"; | ||||||
| import {Utils} from "../../../Utils"; | import {Utils} from "../../../Utils"; | ||||||
| import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"; |  | ||||||
| import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"; | import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"; | ||||||
| import {LayerConfigJson} from "../Json/LayerConfigJson"; | import {LayerConfigJson} from "../Json/LayerConfigJson"; | ||||||
| import Constants from "../../Constants"; | import Constants from "../../Constants"; | ||||||
| import {AllKnownLayouts} from "../../../Customizations/AllKnownLayouts"; | import {DesugaringContext, DesugaringStep, Fuse, OnEvery} from "./Conversion"; | ||||||
| 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 |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string | { builtin, override }> { | 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 Combine from "./Combine"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
|  | import BaseUIElement from "../BaseUIElement"; | ||||||
| 
 | 
 | ||||||
| export default class Loading extends Combine { | export default class Loading extends Combine { | ||||||
|     constructor(msg?: Translation | string) { |     constructor(msg?: BaseUIElement | string) { | ||||||
|         const t = Translations.T(msg) ?? Translations.t.general.loading.Clone(); |         const t = Translations.W(msg) ?? Translations.t.general.loading; | ||||||
|         t.SetClass("pl-2") |         t.SetClass("pl-2") | ||||||
|         super([ |         super([ | ||||||
|             Svg.loading_svg().SetClass("animate-spin").SetStyle("width: 1.5rem; height: 1.5rem;"), |             Svg.loading_svg().SetClass("animate-spin").SetStyle("width: 1.5rem; height: 1.5rem;"), | ||||||
|  |  | ||||||
|  | @ -37,7 +37,7 @@ export default class Minimap { | ||||||
|     /** |     /** | ||||||
|      * Construct a 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()" |         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 _addLayerControl: boolean; | ||||||
|     private readonly _options: MinimapOptions; |     private readonly _options: MinimapOptions; | ||||||
| 
 | 
 | ||||||
|     private constructor(options: MinimapOptions) { |     private constructor(options?: MinimapOptions) { | ||||||
|         super() |         super() | ||||||
|         options = options ?? {} |         options = options ?? {} | ||||||
|         this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined) |         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) |             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) { |         if (self.bounds !== undefined) { | ||||||
|             self.bounds.setData(BBox.fromLeafletBounds(map.getBounds())) |             self.bounds.setData(BBox.fromLeafletBounds(map.getBounds())) | ||||||
|  |  | ||||||
|  | @ -43,6 +43,7 @@ export default class SimpleAddUI extends Toggle { | ||||||
|     constructor(isShown: UIEventSource<boolean>, |     constructor(isShown: UIEventSource<boolean>, | ||||||
|                 filterViewIsOpened: UIEventSource<boolean>, |                 filterViewIsOpened: UIEventSource<boolean>, | ||||||
|                 state: { |                 state: { | ||||||
|  |                     featureSwitchIsTesting: UIEventSource<boolean>, | ||||||
|                     layoutToUse: LayoutConfig, |                     layoutToUse: LayoutConfig, | ||||||
|                     osmConnection: OsmConnection, |                     osmConnection: OsmConnection, | ||||||
|                     changes: Changes, |                     changes: Changes, | ||||||
|  | @ -155,6 +156,7 @@ export default class SimpleAddUI extends Toggle { | ||||||
| 
 | 
 | ||||||
|     private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>, |     private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>, | ||||||
|                                          state: { |                                          state: { | ||||||
|  |                                              featureSwitchIsTesting: UIEventSource<boolean>; | ||||||
|                                              filteredLayers: UIEventSource<FilteredLayer[]>, |                                              filteredLayers: UIEventSource<FilteredLayer[]>, | ||||||
|                                              featureSwitchFilter: UIEventSource<boolean>, |                                              featureSwitchFilter: UIEventSource<boolean>, | ||||||
|                                              osmConnection: OsmConnection |                                              osmConnection: OsmConnection | ||||||
|  | @ -162,10 +164,9 @@ export default class SimpleAddUI extends Toggle { | ||||||
|         const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset) |         const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset) | ||||||
|         let intro: BaseUIElement = Translations.t.general.add.intro; |         let intro: BaseUIElement = Translations.t.general.add.intro; | ||||||
| 
 | 
 | ||||||
|         let testMode: BaseUIElement = undefined; |         let testMode: BaseUIElement = new Toggle(Translations.t.general.testing.SetClass("alert"), | ||||||
|         if (state.osmConnection?.userDetails?.data?.dryRun) { |             undefined, | ||||||
|             testMode = Translations.t.general.testing.Clone().SetClass("alert") |             state.featureSwitchIsTesting); | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col") |         return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -73,10 +73,11 @@ export default class UserBadge extends Toggle { | ||||||
|                     ).SetClass("alert") |                     ).SetClass("alert") | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 let dryrun = new FixedUiElement(""); |                 let dryrun = new Toggle( | ||||||
|                 if (user.dryRun) { |                     new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4"), | ||||||
|                     dryrun = new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4"); |                     undefined, | ||||||
|                 } |                     state.featureSwitchIsTesting | ||||||
|  |                 ) | ||||||
| 
 | 
 | ||||||
|                 const settings = |                 const settings = | ||||||
|                     new Link(Svg.gear, |                     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 * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json" | ||||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | import {GeoOperations} from "../../Logic/GeoOperations"; | ||||||
| import FeatureInfoBox from "../Popup/FeatureInfoBox"; | 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 |  * 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 IsValid | ||||||
|     public readonly Value |     public readonly Value | ||||||
|  | @ -44,19 +46,21 @@ export default class ConflationChecker extends Combine implements FlowStep<any> | ||||||
|         const layer = params.layer; |         const layer = params.layer; | ||||||
|         const toImport = params.geojson; |         const toImport = params.geojson; | ||||||
|         let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached" >("idle") |         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, { |         const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, { | ||||||
|             whenLoaded: (v) => { |             whenLoaded: (v) => { | ||||||
|                 if (v !== undefined) { |                 if (v !== undefined) { | ||||||
|                     console.log("Loaded from local storage:", v) |                     console.log("Loaded from local storage:", v) | ||||||
|                     const [geojson, date] = v; |                     const [geojson, date] = v; | ||||||
|                     const timeDiff = (new Date().getTime() - date.getTime()) / 1000; |                     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) { |                     if (timeDiff < 24 * 60 * 60) { | ||||||
|                         // Recently cached! 
 |                         // Recently cached! 
 | ||||||
|                         overpassStatus.setData("cached") |                         overpassStatus.setData("cached") | ||||||
|                         return; |                         return; | ||||||
|                     } |                     } | ||||||
|  |                     cacheAge.setData(-1) | ||||||
|                 } |                 } | ||||||
|                 // Load the data!
 |                 // Load the data!
 | ||||||
|                 const url = Constants.defaultOverpassUrls[1] |                 const url = Constants.defaultOverpassUrls[1] | ||||||
|  | @ -115,7 +119,7 @@ export default class ConflationChecker extends Combine implements FlowStep<any> | ||||||
|             layerToShow:new LayerConfig(currentview), |             layerToShow:new LayerConfig(currentview), | ||||||
|             state, |             state, | ||||||
|             leafletMap: osmLiveData.leafletMap, |             leafletMap: osmLiveData.leafletMap, | ||||||
|             enablePopups: undefined, |             popup: undefined, | ||||||
|             zoomToFeatures: true, |             zoomToFeatures: true, | ||||||
|             features: new StaticFeatureSource([ |             features: new StaticFeatureSource([ | ||||||
|                 bbox.asGeoJson({}) |                 bbox.asGeoJson({}) | ||||||
|  | @ -161,17 +165,10 @@ export default class ConflationChecker extends Combine implements FlowStep<any> | ||||||
|                 toImport.features.some(imp =>  |                 toImport.features.some(imp =>  | ||||||
|                     maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) ) |                     maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) ) | ||||||
|         }, [nearbyCutoff.GetValue()]), false); |         }, [nearbyCutoff.GetValue()]), false); | ||||||
|  |         const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number)); | ||||||
| 
 | 
 | ||||||
|         // Featuresource showing OSM-features which are nearby a toImport-feature 
 |         // Featuresource showing OSM-features which are nearby a toImport-feature 
 | ||||||
|         const toImportWithNearby = new StaticFeatureSource(geojson.map(osmData => { |         const toImportWithNearby = new StaticFeatureSource(paritionedImport.map(els =>els?.hasNearby ?? []), false); | ||||||
|             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); |  | ||||||
| 
 | 
 | ||||||
|         new ShowDataLayer({ |         new ShowDataLayer({ | ||||||
|             layerToShow:layer, |             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([ |         super([ | ||||||
|             new Title("Comparison with existing data"), |             new Title("Comparison with existing data"), | ||||||
|             new VariableUiElement(overpassStatus.map(d => { |             new VariableUiElement(overpassStatus.map(d => { | ||||||
|  | @ -205,38 +234,19 @@ export default class ConflationChecker extends Combine implements FlowStep<any> | ||||||
|                     return new Loading("Querying overpass...") |                     return new Loading("Querying overpass...") | ||||||
|                 } |                 } | ||||||
|                 if(d === "cached"){ |                 if(d === "cached"){ | ||||||
|                     return new FixedUiElement("Fetched data from local storage") |                     return conflationMaps | ||||||
|                 } |                 } | ||||||
|                 if(d === "success"){ |                 if(d === "success"){ | ||||||
|                     return new FixedUiElement("Data loaded") |                     return conflationMaps | ||||||
|                 } |                 } | ||||||
|                 return new FixedUiElement("Unexpected state "+d).SetClass("alert") |                 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 = paritionedImport.map(feats => ({features: feats?.noNearby, layer: params.layer})) | ||||||
|         this.Value = new UIEventSource(undefined) |         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 {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
| import {FlowStep} from "./FlowStep"; | 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 |  * 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 t = Translations.t.importHelper; | ||||||
| 
 | 
 | ||||||
|         const propertyKeys = new Set<string>() |         const propertyKeys = new Set<string>() | ||||||
|         console.log("Datapanel input got ", geojson) |  | ||||||
|         for (const f of geojson.features) { |         for (const f of geojson.features) { | ||||||
|             Object.keys(f.properties).forEach(key => propertyKeys.add(key)) |             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) |                 !layer.source.osmTags.matchesProperties(f.properties) | ||||||
|             ) |             ) | ||||||
|             if (!mismatched) { |             if (!mismatched) { | ||||||
|  |                 console.log("Autodected layer", layer.id) | ||||||
|                 layerPicker.GetValue().setData(layer); |                 layerPicker.GetValue().setData(layer); | ||||||
|                 layerPicker.GetValue().addCallback(_ => autodetected.setData(false)) |                 layerPicker.GetValue().addCallback(_ => autodetected.setData(false)) | ||||||
|                 autodetected.setData(true) |                 autodetected.setData(true) | ||||||
|  | @ -96,25 +112,22 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer: | ||||||
|         map.SetClass("w-full").SetStyle("height: 500px") |         map.SetClass("w-full").SetStyle("height: 500px") | ||||||
| 
 | 
 | ||||||
|         new ShowDataMultiLayer({ |         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, |                 layerDef: l, | ||||||
|                 isDisplayed: new UIEventSource<boolean>(true), |                 isDisplayed: new UIEventSource<boolean>(true), | ||||||
|                 appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined) |                 appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined) | ||||||
|             }))), |             }))), | ||||||
|             zoomToFeatures: true, |             zoomToFeatures: true, | ||||||
|             features: new StaticFeatureSource(matching, false), |             features: new StaticFeatureSource(matching, false), | ||||||
|             state: { |  | ||||||
|                 ...state, |  | ||||||
|                 filteredLayers: new UIEventSource<FilteredLayer[]>(undefined), |  | ||||||
|                 backgroundLayer: background |  | ||||||
|             }, |  | ||||||
|             leafletMap: map.leafletMap, |             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])))) |         var bbox = matching.map(feats => BBox.bboxAroundAll(feats.map(f => new BBox([f.geometry.coordinates])))) | ||||||
| 
 | 
 | ||||||
|         super([ |         super([ | ||||||
|             "Has " + geojson.features.length + " features", |             new Title(geojson.features.length + " features to import"), | ||||||
|             layerPicker, |             layerPicker, | ||||||
|             new Toggle("Automatically detected layer", undefined, autodetected), |             new Toggle("Automatically detected layer", undefined, autodetected), | ||||||
|             new Table(["", "Key", "Values", "Unique values seen"], |             new Table(["", "Key", "Values", "Unique values seen"], | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
| import Toggle from "../Input/Toggle"; | import Toggle from "../Input/Toggle"; | ||||||
| import {UIElement} from "../UIElement"; | import {UIElement} from "../UIElement"; | ||||||
| 
 | 
 | ||||||
| export interface FlowStep<T> extends BaseUIElement{ | export interface FlowStep<T> extends BaseUIElement { | ||||||
|     readonly IsValid: UIEventSource<boolean> |     readonly IsValid: UIEventSource<boolean> | ||||||
|     readonly Value: UIEventSource<T> |     readonly Value: UIEventSource<T> | ||||||
| } | } | ||||||
|  | @ -16,19 +16,19 @@ export interface FlowStep<T> extends BaseUIElement{ | ||||||
| export class FlowPanelFactory<T> { | export class FlowPanelFactory<T> { | ||||||
|     private _initial: FlowStep<any>; |     private _initial: FlowStep<any>; | ||||||
|     private _steps: ((x: any) => FlowStep<any>)[]; |     private _steps: ((x: any) => FlowStep<any>)[]; | ||||||
|     private _stepNames: string[]; |     private _stepNames: (string | BaseUIElement)[]; | ||||||
| 
 | 
 | ||||||
|     private constructor(initial: FlowStep<any>, steps: ((x:any) => FlowStep<any>)[], stepNames: string[]) { |     private constructor(initial: FlowStep<any>, steps: ((x: any) => FlowStep<any>)[], stepNames: (string | BaseUIElement)[]) { | ||||||
|         this._initial = initial; |         this._initial = initial; | ||||||
|         this._steps = steps; |         this._steps = steps; | ||||||
|         this._stepNames = stepNames; |         this._stepNames = stepNames; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static start<TOut> (step: FlowStep<TOut>): FlowPanelFactory<TOut>{ |     public static start<TOut>(name: string | BaseUIElement, step: FlowStep<TOut>): FlowPanelFactory<TOut> { | ||||||
|         return new FlowPanelFactory(step, [], []) |         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>( |         return new FlowPanelFactory<TOut>( | ||||||
|             this._initial, |             this._initial, | ||||||
|             this._steps.concat([construct]), |             this._steps.concat([construct]), | ||||||
|  | @ -36,50 +36,77 @@ export class FlowPanelFactory<T> { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     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)
 |         // 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) |         nextConstr.push(construct) | ||||||
|          |         for (let i = this._steps.length - 1; i >= 0; i--) { | ||||||
|         for (let i = this._steps.length - 1; i >= 0; i--){ |             const createFlowStep: (value) => FlowStep<any> = this._steps[i]; | ||||||
|             const createFlowStep : (value) => FlowStep<any> = this._steps[i]; |             const isConfirm = i == this._steps.length - 1; | ||||||
|             nextConstr[i] = (value, backButton) => { |             nextConstr[i] = (value, backButton) => { | ||||||
|                 console.log("Creating flowSTep ", this._stepNames[i]) |  | ||||||
|                 const flowStep = createFlowStep(value) |                 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 { | export class FlowPanel<T> extends Toggle { | ||||||
|  |     public isActive: UIEventSource<boolean> | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         initial: (FlowStep<T>), |         initial: (FlowStep<T>), | ||||||
|         constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement), |         constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement), | ||||||
|         backbutton?: BaseUIElement |         backbutton?: BaseUIElement, | ||||||
|  |         isConfirm = false | ||||||
|     ) { |     ) { | ||||||
|         const t = Translations.t.general; |         const t = Translations.t.general; | ||||||
| 
 | 
 | ||||||
|         const currentStepActive = new UIEventSource(true); |         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(() => { |         const backButtonForNextStep = new SubtleButton(Svg.back_svg(), t.back).onClick(() => { | ||||||
|             currentStepActive.setData(true) |             currentStepActive.setData(true) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         let elements : (BaseUIElement | string)[] = [] |         let elements: (BaseUIElement | string)[] = [] | ||||||
|         if(initial !== undefined){ |         if (initial !== undefined) { | ||||||
|             // Startup the flow
 |             // Startup the flow
 | ||||||
|             elements = [ |             elements = [ | ||||||
|                 initial, |                 initial, | ||||||
|                 new Combine([ |                 new Combine([ | ||||||
|                     backbutton, |                     backbutton, | ||||||
|                     new Toggle( |                     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; |                             const v = initial.Value.data; | ||||||
|                             nextStep.setData(constructNextstep(v, backButtonForNextStep)) |                             nextStep.setData(constructNextstep(v, backButtonForNextStep)) | ||||||
|                             currentStepActive.setData(false) |                             currentStepActive.setData(false) | ||||||
|  | @ -98,8 +125,8 @@ export class FlowPanel<T> extends Toggle { | ||||||
|             new VariableUiElement(nextStep), |             new VariableUiElement(nextStep), | ||||||
|             currentStepActive |             currentStepActive | ||||||
|         ); |         ); | ||||||
|  |         this.isActive = currentStepActive | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|      |  | ||||||
| } | } | ||||||
|  | @ -9,11 +9,18 @@ import MoreScreen from "../BigComponents/MoreScreen"; | ||||||
| import MinimapImplementation from "../Base/MinimapImplementation"; | import MinimapImplementation from "../Base/MinimapImplementation"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants"; | ||||||
| import {FlowPanel, FlowPanelFactory} from "./FlowStep"; | import {FlowPanelFactory} from "./FlowStep"; | ||||||
| import {RequestFile} from "./RequestFile"; | import {RequestFile} from "./RequestFile"; | ||||||
| import {DataPanel} from "./DataPanel"; | import {DataPanel} from "./DataPanel"; | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; |  | ||||||
| import ConflationChecker from "./ConflationChecker"; | 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 { | export default class ImportHelperGui extends LoginToggle { | ||||||
|     constructor() { |     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
 |         // We disable the userbadge, as various 'showData'-layers will give a read-only view in this case
 | ||||||
|         state.featureSwitchUserbadge.setData(false) |         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[] = [ |         const leftContents: BaseUIElement[] = [ | ||||||
|             new BackToIndex().SetClass("block pl-4"), |             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"), |             LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"), | ||||||
|         ].map(el => el?.SetClass("pl-4")) |         ].map(el => el?.SetClass("pl-4")) | ||||||
| 
 | 
 | ||||||
|         const leftBar = new Combine([ |         const leftBar = new Combine([ | ||||||
|             new Combine(leftContents).SetClass("sticky top-4 m-4") |             new Combine(leftContents).SetClass("sticky top-4 m-4"), | ||||||
|           ]).SetClass("block w-full md:w-2/6 lg:w-1/6") |           ]).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( |         super( | ||||||
|             new Toggle( |             new Toggle( | ||||||
|                 new Combine([ |                 new Combine([ | ||||||
|                     leftBar, |                     leftBar, | ||||||
|                     mainPanel.SetClass("m-8 w-full mb-24") |                     flow.SetClass("m-8 w-full mb-24") | ||||||
|                 ]).SetClass("h-full block md:flex") |                 ]).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>, |         mapBackgroundLayer?: UIEventSource<any>, | ||||||
|         unit?: Unit, |         unit?: Unit, | ||||||
|         args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
 |         args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
 | ||||||
|         feature?: any |         feature?: any, | ||||||
|  |         inputStyle?: string | ||||||
|     }): InputElement<string> { |     }): InputElement<string> { | ||||||
|         options = options ?? {}; |         options = options ?? {}; | ||||||
|         options.placeholder = options.placeholder ?? type; |         options.placeholder = options.placeholder ?? type; | ||||||
|  |  | ||||||
|  | @ -19,6 +19,7 @@ export default class ConfirmLocationOfPoint extends Combine { | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         state: { |         state: { | ||||||
|  |             featureSwitchIsTesting: UIEventSource<boolean>; | ||||||
|             osmConnection: OsmConnection, |             osmConnection: OsmConnection, | ||||||
|             featurePipeline: FeaturePipeline, |             featurePipeline: FeaturePipeline, | ||||||
|             backgroundLayer?: UIEventSource<BaseLayer> |             backgroundLayer?: UIEventSource<BaseLayer> | ||||||
|  | @ -167,8 +168,11 @@ export default class ConfirmLocationOfPoint extends Combine { | ||||||
|         ).onClick(cancel) |         ).onClick(cancel) | ||||||
| 
 | 
 | ||||||
|         super([ |         super([ | ||||||
|             state.osmConnection.userDetails.data.dryRun ? |             new Toggle( | ||||||
|                 Translations.t.general.testing.Clone().SetClass("alert") : undefined, |                 Translations.t.general.testing.SetClass("alert"), | ||||||
|  |                 undefined, | ||||||
|  |                 state.featureSwitchIsTesting | ||||||
|  |             ), | ||||||
|             disableFiltersOrConfirm, |             disableFiltersOrConfirm, | ||||||
|             cancelButton, |             cancelButton, | ||||||
|             preset.description, |             preset.description, | ||||||
|  |  | ||||||
|  | @ -141,7 +141,7 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
|         if(tagSpec.indexOf(" ")< 0 && tagSpec.indexOf(";") < 0 && tagSource.data[args.tags] !== undefined){ |         if(tagSpec.indexOf(" ")< 0 && tagSpec.indexOf(";") < 0 && tagSource.data[args.tags] !== undefined){ | ||||||
|             // This is probably a key
 |             // This is probably a key
 | ||||||
|             tagSpec = tagSource.data[args.tags] |             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); |         const importClicked = new UIEventSource(false); | ||||||
|  | @ -201,7 +201,7 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
|             if(tags.indexOf(" ") < 0 && tags.indexOf(";") < 0 && originalFeatureTags.data[tags] !== undefined){ |             if(tags.indexOf(" ") < 0 && tags.indexOf(";") < 0 && originalFeatureTags.data[tags] !== undefined){ | ||||||
|                 // This might be a property to expand...
 |                 // This might be a property to expand...
 | ||||||
|                 const items : string = originalFeatureTags.data[tags] |                 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) |                 baseArgs["newTags"] = TagApplyButton.generateTagsToApply(items, originalFeatureTags) | ||||||
|             }else{ |             }else{ | ||||||
|                 baseArgs["newTags"] = TagApplyButton.generateTagsToApply(tags, originalFeatureTags) |                 baseArgs["newTags"] = TagApplyButton.generateTagsToApply(tags, originalFeatureTags) | ||||||
|  |  | ||||||
|  | @ -55,6 +55,45 @@ export interface SpecialVisualization { | ||||||
|     getLayerDependencies?: (argument: string[]) => string[] |     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 { | export default class SpecialVisualizations { | ||||||
| 
 | 
 | ||||||
|     public static specialVisualizations = SpecialVisualizations.init() |     public static specialVisualizations = SpecialVisualizations.init() | ||||||
|  | @ -99,37 +138,7 @@ export default class SpecialVisualizations { | ||||||
|                     funcName: "all_tags", |                     funcName: "all_tags", | ||||||
|                     docs: "Prints all key-value pairs of the object - used for debugging", |                     docs: "Prints all key-value pairs of the object - used for debugging", | ||||||
|                     args: [], |                     args: [], | ||||||
|                     constr: ((state, tags: UIEventSource<any>) => { |                     constr: ((state, tags: UIEventSource<any>) => new AllTagsPanel(tags, state)) | ||||||
|                         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") |  | ||||||
|                     }) |  | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     funcName: "image_carousel", |                     funcName: "image_carousel", | ||||||
|  | @ -339,7 +348,7 @@ export default class SpecialVisualizations { | ||||||
|                         const mangrove = MangroveReviews.Get(Number(tgs._lon), Number(tgs._lat), |                         const mangrove = MangroveReviews.Get(Number(tgs._lon), Number(tgs._lat), | ||||||
|                             encodeURIComponent(subject), |                             encodeURIComponent(subject), | ||||||
|                             state.mangroveIdentity, |                             state.mangroveIdentity, | ||||||
|                             state.osmConnection._dryRun |                             state.featureSwitchIsTesting.data | ||||||
|                         ); |                         ); | ||||||
|                         const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection); |                         const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection); | ||||||
|                         return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form); |                         return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form); | ||||||
|  | @ -743,10 +752,6 @@ export default class SpecialVisualizations { | ||||||
|                             return t.addCommentAndClose |                             return t.addCommentAndClose | ||||||
|                         }))).onClick(() => { |                         }))).onClick(() => { | ||||||
|                             const id = tags.data[args[1] ?? "id"] |                             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(_ => { |                             state.osmConnection.closeNote(id, txt.data).then(_ => { | ||||||
|                                 tags.data["closed_at"] = new Date().toISOString(); |                                 tags.data["closed_at"] = new Date().toISOString(); | ||||||
|                                 tags.ping() |                                 tags.ping() | ||||||
|  | @ -760,10 +765,6 @@ export default class SpecialVisualizations { | ||||||
|                             return t.reopenNoteAndComment |                             return t.reopenNoteAndComment | ||||||
|                         }))).onClick(() => { |                         }))).onClick(() => { | ||||||
|                             const id = tags.data[args[1] ?? "id"] |                             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(_ => { |                             state.osmConnection.reopenNote(id, txt.data).then(_ => { | ||||||
|                                 tags.data["closed_at"] = undefined; |                                 tags.data["closed_at"] = undefined; | ||||||
|                                 tags.ping() |                                 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)", |   "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": { |   "source": { | ||||||
|     "osmTags": "id~*", |     "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, |     "geoJsonZoomLevel": 12, | ||||||
|     "maxCacheAge": 0 |     "maxCacheAge": 0 | ||||||
|   }, |   }, | ||||||
|  | @ -29,7 +29,8 @@ | ||||||
|     "_opened_by_anonymous_user:=feat.get('comments')[0].user === undefined", |     "_opened_by_anonymous_user:=feat.get('comments')[0].user === undefined", | ||||||
|     "_first_user:=feat.get('comments')[0].user", |     "_first_user:=feat.get('comments')[0].user", | ||||||
|     "_first_user_lc:=feat.get('comments')[0].user?.toLowerCase()", |     "_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": [ |   "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; |   padding: 0.125rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .p-8 { | ||||||
|  |   padding: 2rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .pb-12 { | .pb-12 { | ||||||
|   padding-bottom: 3rem; |   padding-bottom: 3rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -465,7 +465,9 @@ | ||||||
|     "importLayer": { |     "importLayer": { | ||||||
|         "layerName": "Possible {title}", |         "layerName": "Possible {title}", | ||||||
|         "description": "A layer which imports entries for {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": { |     "importHelper": { | ||||||
|         "title": "Import helper", |         "title": "Import helper", | ||||||
|  |  | ||||||
|  | @ -3354,6 +3354,13 @@ | ||||||
|                         "question": "Only show open notes" |                         "question": "Only show open notes" | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |             }, | ||||||
|  |             "8": { | ||||||
|  |                 "options": { | ||||||
|  |                     "0": { | ||||||
|  |                         "question": "Hide import notes" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "name": "OpenStreetMap notes", |         "name": "OpenStreetMap notes", | ||||||
|  |  | ||||||
|  | @ -314,5 +314,12 @@ | ||||||
|     }, |     }, | ||||||
|     "multi_apply": { |     "multi_apply": { | ||||||
|         "autoApply": "Wijzigingen aan eigenschappen {attr_names} zullen ook worden uitgevoerd op {count} andere objecten." |         "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 {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | ||||||
| import Constants from "../Models/Constants"; | import Constants from "../Models/Constants"; | ||||||
| import { | import { | ||||||
|     DesugaringContext, |  | ||||||
|     PrepareLayer, |  | ||||||
|     PrepareTheme, |  | ||||||
|     ValidateLayer, |     ValidateLayer, | ||||||
|     ValidateThemeAndLayers |     ValidateThemeAndLayers | ||||||
| } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; | } 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 questions from "../assets/tagRenderings/questions.json"; | ||||||
| import * as icons from "../assets/tagRenderings/icons.json"; | import * as icons from "../assets/tagRenderings/icons.json"; | ||||||
| import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson"; | 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.
 | // 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
 | // It spits out an overview of those to be used to load them
 | ||||||
|  |  | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| import T from "./TestHelper"; | import T from "./TestHelper"; | ||||||
| import CreateNoteImportLayer from "../Models/ThemeConfig/Conversion/CreateNoteImportLayer"; | import CreateNoteImportLayer from "../Models/ThemeConfig/Conversion/CreateNoteImportLayer"; | ||||||
| import * as bookcases from "../assets/layers/public_bookcase/public_bookcase.json" | 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 {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | ||||||
| import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; | import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; | ||||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||||
|  | import {PrepareLayer} from "../Models/ThemeConfig/Conversion/PrepareLayer"; | ||||||
| 
 | 
 | ||||||
| export default class CreateNoteImportLayerSpec extends T { | export default class CreateNoteImportLayerSpec extends T { | ||||||
| 
 | 
 | ||||||
|  | @ -17,7 +18,7 @@ export default class CreateNoteImportLayerSpec extends T { | ||||||
| 
 | 
 | ||||||
|                 } |                 } | ||||||
|                 const layerPrepare = new PrepareLayer() |                 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 generator = new CreateNoteImportLayer() | ||||||
|                 const generatedLayer = generator.convertStrict(desugaringState, layer, "ImportLayerGeneratorTest: convert") |                 const generatedLayer = generator.convertStrict(desugaringState, layer, "ImportLayerGeneratorTest: convert") | ||||||
|        //         fs.writeFileSync("bookcases-import-layer.generated.json", JSON.stringify(generatedLayer, null, "  "), "utf8")
 |        //         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": "5"}), true); | ||||||
|                 equal(compare.matchesProperties({"key": "4.2"}), false); |                 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", (() => { |             ["Is equivalent test", (() => { | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,10 +3,10 @@ import * as assert from "assert"; | ||||||
| import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; | import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; | ||||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||||
| import * as bookcaseLayer from "../assets/generated/layers/public_bookcase.json" | 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 {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; | ||||||
| import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | ||||||
| import Constants from "../Models/Constants"; | import Constants from "../Models/Constants"; | ||||||
|  | import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme"; | ||||||
| 
 | 
 | ||||||
| export default class ThemeSpec extends T { | export default class ThemeSpec extends T { | ||||||
|     constructor() { |     constructor() { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue