forked from MapComplete/MapComplete
		
	removed geojson
This commit is contained in:
		
							parent
							
								
									4d0f0e55f2
								
							
						
					
					
						commit
						d5b614fc44
					
				
					 88 changed files with 1835 additions and 4874 deletions
				
			
		
							
								
								
									
										
											BIN
										
									
								
								.DS_Store
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								.DS_Store
									
										
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -3,7 +3,6 @@ import UnitConfigJson from "./UnitConfigJson"; | ||||||
| import Translations from "../../UI/i18n/Translations"; | import Translations from "../../UI/i18n/Translations"; | ||||||
| import BaseUIElement from "../../UI/BaseUIElement"; | import BaseUIElement from "../../UI/BaseUIElement"; | ||||||
| import Combine from "../../UI/Base/Combine"; | import Combine from "../../UI/Base/Combine"; | ||||||
| import {FixedUiElement} from "../../UI/Base/FixedUiElement"; |  | ||||||
| 
 | 
 | ||||||
| export class Unit { | export class Unit { | ||||||
|     public readonly appliesToKeys: Set<string>; |     public readonly appliesToKeys: Set<string>; | ||||||
|  | @ -82,10 +81,7 @@ export class Unit { | ||||||
|             return undefined; |             return undefined; | ||||||
|         } |         } | ||||||
|         const [stripped, denom] = this.findDenomination(value) |         const [stripped, denom] = this.findDenomination(value) | ||||||
|         const human = denom?.human |         const human = denom.human | ||||||
|         if(human === undefined){ |  | ||||||
|             return new FixedUiElement(stripped ?? value); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         const elems = denom.prefix ? [human, stripped] : [stripped, human]; |         const elems = denom.prefix ? [human, stripped] : [stripped, human]; | ||||||
|         return new Combine(elems) |         return new Combine(elems) | ||||||
|  | @ -156,7 +152,7 @@ export class Denomination { | ||||||
|         if (stripped === null) { |         if (stripped === null) { | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
|         return (stripped + " " + this.canonical.trim()).trim(); |         return stripped + " " + this.canonical.trim() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -1,27 +0,0 @@ | ||||||
| import { TagsFilter } from "../../Logic/Tags/TagsFilter"; |  | ||||||
| import { Translation } from "../../UI/i18n/Translation"; |  | ||||||
| import Translations from "../../UI/i18n/Translations"; |  | ||||||
| import FilterConfigJson from "./FilterConfigJson"; |  | ||||||
| import { FromJSON } from "./FromJSON"; |  | ||||||
| 
 |  | ||||||
| export default class FilterConfig { |  | ||||||
|   readonly options: { |  | ||||||
|     question: Translation; |  | ||||||
|     osmTags: TagsFilter; |  | ||||||
|   }[]; |  | ||||||
| 
 |  | ||||||
|   constructor(json: FilterConfigJson, context: string) { |  | ||||||
|     this.options = json.options.map((option, i) => { |  | ||||||
|       const question = Translations.T( |  | ||||||
|         option.question, |  | ||||||
|         context + ".options-[" + i + "].question" |  | ||||||
|       ); |  | ||||||
|       const osmTags = FromJSON.Tag( |  | ||||||
|         option.osmTags, |  | ||||||
|         `${context}.options-[${i}].osmTags` |  | ||||||
|       ); |  | ||||||
| 
 |  | ||||||
|       return { question: question, osmTags: osmTags }; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -1,11 +0,0 @@ | ||||||
| import { AndOrTagConfigJson } from "./TagConfigJson"; |  | ||||||
| 
 |  | ||||||
| export default interface FilterConfigJson { |  | ||||||
|   /** |  | ||||||
|    * The options for a filter |  | ||||||
|    * If there are multiple options these will be a list of radio buttons |  | ||||||
|    * If there is only one option this will be a checkbox |  | ||||||
|    * Filtering is done based on the given osmTags that are compared to the objects in that layer. |  | ||||||
|    */ |  | ||||||
|   options: { question: string | any; osmTags: AndOrTagConfigJson | string }[]; |  | ||||||
| } |  | ||||||
|  | @ -18,18 +18,19 @@ import {Tag} from "../../Logic/Tags/Tag"; | ||||||
| import BaseUIElement from "../../UI/BaseUIElement"; | import BaseUIElement from "../../UI/BaseUIElement"; | ||||||
| import {Unit} from "./Denomination"; | import {Unit} from "./Denomination"; | ||||||
| import DeleteConfig from "./DeleteConfig"; | import DeleteConfig from "./DeleteConfig"; | ||||||
| import FilterConfig from "./FilterConfig"; |  | ||||||
| 
 | 
 | ||||||
| export default class LayerConfig { | export default class LayerConfig { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     static WAYHANDLING_DEFAULT = 0; |     static WAYHANDLING_DEFAULT = 0; | ||||||
|     static WAYHANDLING_CENTER_ONLY = 1; |     static WAYHANDLING_CENTER_ONLY = 1; | ||||||
|     static WAYHANDLING_CENTER_AND_WAY = 2; |     static WAYHANDLING_CENTER_AND_WAY = 2; | ||||||
| 
 | 
 | ||||||
|     id: string; |     id: string; | ||||||
|     name: Translation; |     name: Translation | ||||||
|     description: Translation; |     description: Translation; | ||||||
|     source: SourceConfig; |     source: SourceConfig; | ||||||
|     calculatedTags: [string, string][]; |     calculatedTags: [string, string][] | ||||||
|     doNotDownload: boolean; |     doNotDownload: boolean; | ||||||
|     passAllFeatures: boolean; |     passAllFeatures: boolean; | ||||||
|     isShown: TagRenderingConfig; |     isShown: TagRenderingConfig; | ||||||
|  | @ -38,7 +39,7 @@ export default class LayerConfig { | ||||||
|     title?: TagRenderingConfig; |     title?: TagRenderingConfig; | ||||||
|     titleIcons: TagRenderingConfig[]; |     titleIcons: TagRenderingConfig[]; | ||||||
|     icon: TagRenderingConfig; |     icon: TagRenderingConfig; | ||||||
|     iconOverlays: { if: TagsFilter; then: TagRenderingConfig; badge: boolean }[]; |     iconOverlays: { if: TagsFilter, then: TagRenderingConfig, badge: boolean }[] | ||||||
|     iconSize: TagRenderingConfig; |     iconSize: TagRenderingConfig; | ||||||
|     label: TagRenderingConfig; |     label: TagRenderingConfig; | ||||||
|     rotation: TagRenderingConfig; |     rotation: TagRenderingConfig; | ||||||
|  | @ -47,24 +48,20 @@ export default class LayerConfig { | ||||||
|     dashArray: TagRenderingConfig; |     dashArray: TagRenderingConfig; | ||||||
|     wayHandling: number; |     wayHandling: number; | ||||||
|     public readonly units: Unit[]; |     public readonly units: Unit[]; | ||||||
|     public readonly deletion: DeleteConfig | null; |     public readonly deletion: DeleteConfig | null | ||||||
| 
 | 
 | ||||||
|     presets: { |     presets: { | ||||||
|         title: Translation, |         title: Translation, | ||||||
|         tags: Tag[], |         tags: Tag[], | ||||||
|         description?: Translation, |         description?: Translation, | ||||||
|         preciseInput?: { preferredBackground?: string } |  | ||||||
|     }[]; |     }[]; | ||||||
| 
 | 
 | ||||||
|     tagRenderings: TagRenderingConfig []; |     tagRenderings: TagRenderingConfig []; | ||||||
|     filters: FilterConfig[]; |  | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor(json: LayerConfigJson, | ||||||
|         json: LayerConfigJson, |  | ||||||
|                 units?:Unit[], |                 units?:Unit[], | ||||||
|                 context?: string, |                 context?: string, | ||||||
|         official: boolean = true |                 official: boolean = true,) { | ||||||
|     ) { |  | ||||||
|         this.units = units ?? []; |         this.units = units ?? []; | ||||||
|         context = context + "." + json.id; |         context = context + "." + json.id; | ||||||
|         const self = this; |         const self = this; | ||||||
|  | @ -77,10 +74,7 @@ export default class LayerConfig { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         this.description = Translations.T( |         this.description =Translations.T(json.description, context + ".description") ;  | ||||||
|             json.description, |  | ||||||
|             context + ".description" |  | ||||||
|         ); |  | ||||||
| 
 | 
 | ||||||
|         let legacy = undefined; |         let legacy = undefined; | ||||||
|         if (json["overpassTags"] !== undefined) { |         if (json["overpassTags"] !== undefined) { | ||||||
|  | @ -89,54 +83,45 @@ export default class LayerConfig { | ||||||
|         } |         } | ||||||
|         if (json.source !== undefined) { |         if (json.source !== undefined) { | ||||||
|             if (legacy !== undefined) { |             if (legacy !== undefined) { | ||||||
|                 throw ( |                 throw context + "Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined" | ||||||
|                     context + |  | ||||||
|                     "Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined" |  | ||||||
|                 ); |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             let osmTags: TagsFilter = legacy; |             let osmTags: TagsFilter = legacy; | ||||||
|             if (json.source["osmTags"]) { |             if (json.source["osmTags"]) { | ||||||
|                 osmTags = FromJSON.Tag( |                 osmTags = FromJSON.Tag(json.source["osmTags"], context + "source.osmTags"); | ||||||
|                     json.source["osmTags"], |  | ||||||
|                     context + "source.osmTags" |  | ||||||
|                 ); |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if(json.source["geoJsonSource"] !== undefined){ |             if(json.source["geoJsonSource"] !== undefined){ | ||||||
|                 throw context + "Use 'geoJson' instead of 'geoJsonSource'"; |                 throw context + "Use 'geoJson' instead of 'geoJsonSource'" | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             this.source = new SourceConfig( |             this.source = new SourceConfig({ | ||||||
|                 { |  | ||||||
|                 osmTags: osmTags, |                 osmTags: osmTags, | ||||||
|                 geojsonSource: json.source["geoJson"], |                 geojsonSource: json.source["geoJson"], | ||||||
|                 geojsonSourceLevel: json.source["geoJsonZoomLevel"], |                 geojsonSourceLevel: json.source["geoJsonZoomLevel"], | ||||||
|                 overpassScript: json.source["overpassScript"], |                 overpassScript: json.source["overpassScript"], | ||||||
|                     isOsmCache: json.source["isOsmCache"], |                 isOsmCache: json.source["isOsmCache"] | ||||||
|                 }, |             }, this.id); | ||||||
|                 this.id |  | ||||||
|             ); |  | ||||||
|         } else { |         } else { | ||||||
|             this.source = new SourceConfig({ |             this.source = new SourceConfig({ | ||||||
|                 osmTags: legacy, |                 osmTags: legacy | ||||||
|             }); |             }) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         this.calculatedTags = undefined; |         this.calculatedTags = undefined; | ||||||
|         if (json.calculatedTags !== undefined) { |         if (json.calculatedTags !== undefined) { | ||||||
|             if (!official) { |             if (!official) { | ||||||
|                 console.warn( |                 console.warn(`Unofficial theme ${this.id} with custom javascript! This is a security risk`) | ||||||
|                     `Unofficial theme ${this.id} with custom javascript! This is a security risk` |  | ||||||
|                 ); |  | ||||||
|             } |             } | ||||||
|             this.calculatedTags = []; |             this.calculatedTags = []; | ||||||
|             for (const kv of json.calculatedTags) { |             for (const kv of json.calculatedTags) { | ||||||
|                 const index = kv.indexOf("="); | 
 | ||||||
|  |                 const index = kv.indexOf("=") | ||||||
|                 const key = kv.substring(0, index); |                 const key = kv.substring(0, index); | ||||||
|                 const code = kv.substring(index + 1); |                 const code = kv.substring(index + 1); | ||||||
|                  |                  | ||||||
|                 this.calculatedTags.push([key, code]); |                 this.calculatedTags.push([key, code]) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -145,19 +130,13 @@ export default class LayerConfig { | ||||||
|         this.minzoom = json.minzoom ?? 0; |         this.minzoom = json.minzoom ?? 0; | ||||||
|         this.maxzoom = json.maxzoom ?? 1000; |         this.maxzoom = json.maxzoom ?? 1000; | ||||||
|         this.wayHandling = json.wayHandling ?? 0; |         this.wayHandling = json.wayHandling ?? 0; | ||||||
|         this.presets = (json.presets ?? []).map((pr, i) => { |         this.presets = (json.presets ?? []).map((pr, i) => | ||||||
|             if (pr.preciseInput === true) { |             ({ | ||||||
|                 pr.preciseInput = { |  | ||||||
|                     preferredBackground: undefined |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             return { |  | ||||||
|                 title: Translations.T(pr.title, `${context}.presets[${i}].title`), |                 title: Translations.T(pr.title, `${context}.presets[${i}].title`), | ||||||
|                 tags: pr.tags.map((t) => FromJSON.SimpleTag(t)), |                 tags: pr.tags.map(t => FromJSON.SimpleTag(t)), | ||||||
|                 description: Translations.T(pr.description, `${context}.presets[${i}].description`), |                 description: Translations.T(pr.description, `${context}.presets[${i}].description`) | ||||||
|                 preciseInput: pr.preciseInput |             })) | ||||||
|             } | 
 | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         /** Given a key, gets the corresponding property from the json (or the default if not found |         /** Given a key, gets the corresponding property from the json (or the default if not found | ||||||
|          * |          * | ||||||
|  | @ -169,11 +148,7 @@ export default class LayerConfig { | ||||||
|                 if (deflt === undefined) { |                 if (deflt === undefined) { | ||||||
|                     return undefined; |                     return undefined; | ||||||
|                 } |                 } | ||||||
|                 return new TagRenderingConfig( |                 return new TagRenderingConfig(deflt, self.source.osmTags, `${context}.${key}.default value`); | ||||||
|                     deflt, |  | ||||||
|                     self.source.osmTags, |  | ||||||
|                     `${context}.${key}.default value` |  | ||||||
|                 ); |  | ||||||
|             } |             } | ||||||
|             if (typeof v === "string") { |             if (typeof v === "string") { | ||||||
|                 const shared = SharedTagRenderings.SharedTagRendering.get(v); |                 const shared = SharedTagRenderings.SharedTagRendering.get(v); | ||||||
|  | @ -181,80 +156,54 @@ export default class LayerConfig { | ||||||
|                     return shared; |                     return shared; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             return new TagRenderingConfig( |             return new TagRenderingConfig(v, self.source.osmTags, `${context}.${key}`); | ||||||
|                 v, |  | ||||||
|                 self.source.osmTags, |  | ||||||
|                 `${context}.${key}` |  | ||||||
|             ); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|          * Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig |          * Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig | ||||||
|          * A string is interpreted as a name to call |          * A string is interpreted as a name to call | ||||||
|          */ |          */ | ||||||
|         function trs( |         function trs(tagRenderings?: (string | TagRenderingConfigJson)[], readOnly = false) { | ||||||
|             tagRenderings?: (string | TagRenderingConfigJson)[], |  | ||||||
|             readOnly = false |  | ||||||
|         ) { |  | ||||||
|             if (tagRenderings === undefined) { |             if (tagRenderings === undefined) { | ||||||
|                 return []; |                 return []; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return Utils.NoNull( |             return Utils.NoNull(tagRenderings.map( | ||||||
|                 tagRenderings.map((renderingJson, i) => { |                 (renderingJson, i) => { | ||||||
|                     if (typeof renderingJson === "string") { |                     if (typeof renderingJson === "string") { | ||||||
|  | 
 | ||||||
|                         if (renderingJson === "questions") { |                         if (renderingJson === "questions") { | ||||||
|                             if (readOnly) { |                             if (readOnly) { | ||||||
|                                 throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}. The offending tagrendering is ${JSON.stringify( |                                 throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}. The offending tagrendering is ${JSON.stringify(renderingJson)}` | ||||||
|                                     renderingJson |  | ||||||
|                                 )}`;
 |  | ||||||
|                             } |                             } | ||||||
| 
 | 
 | ||||||
|                             return new TagRenderingConfig("questions", undefined); |                             return new TagRenderingConfig("questions", undefined) | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
|                         const shared = | 
 | ||||||
|                             SharedTagRenderings.SharedTagRendering.get(renderingJson); |                         const shared = SharedTagRenderings.SharedTagRendering.get(renderingJson); | ||||||
|                         if (shared !== undefined) { |                         if (shared !== undefined) { | ||||||
|                             return shared; |                             return shared; | ||||||
|                         } |                         } | ||||||
|                          |                          | ||||||
|                         const keys = Array.from( |                         const keys = Array.from(SharedTagRenderings.SharedTagRendering.keys()) | ||||||
|                             SharedTagRenderings.SharedTagRendering.keys() |  | ||||||
|                         ); |  | ||||||
|                          |                          | ||||||
|                         if(Utils.runningFromConsole){ |                         if(Utils.runningFromConsole){ | ||||||
|                             return undefined; |                             return undefined; | ||||||
|                         } |                         } | ||||||
|                          |                          | ||||||
|                         throw `Predefined tagRendering ${renderingJson} not found in ${context}.\n    Try one of ${keys.join( |                         throw `Predefined tagRendering ${renderingJson} not found in ${context}.\n    Try one of ${(keys.join(", "))}\n    If you intent to output this text literally, use {\"render\": <your text>} instead"}`; | ||||||
|                             ", " |  | ||||||
|                         )}\n    If you intent to output this text literally, use {\"render\": <your text>} instead"}`;
 |  | ||||||
|                     } |                     } | ||||||
|                     return new TagRenderingConfig( |                     return new TagRenderingConfig(renderingJson, self.source.osmTags, `${context}.tagrendering[${i}]`); | ||||||
|                         renderingJson, |                 })); | ||||||
|                         self.source.osmTags, |  | ||||||
|                         `${context}.tagrendering[${i}]` |  | ||||||
|                     ); |  | ||||||
|                 }) |  | ||||||
|             ); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.tagRenderings = trs(json.tagRenderings, false); |         this.tagRenderings = trs(json.tagRenderings, false); | ||||||
| 
 | 
 | ||||||
|         this.filters = (json.filter ?? []).map((option, i) => { |  | ||||||
|             return new FilterConfig(option, `${context}.filter-[${i}]`) |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         const titleIcons = []; |         const titleIcons = []; | ||||||
|         const defaultIcons = [ |         const defaultIcons = ["phonelink", "emaillink", "wikipedialink", "osmlink", "sharelink"]; | ||||||
|             "phonelink", |         for (const icon of (json.titleIcons ?? defaultIcons)) { | ||||||
|             "emaillink", |  | ||||||
|             "wikipedialink", |  | ||||||
|             "osmlink", |  | ||||||
|             "sharelink", |  | ||||||
|         ]; |  | ||||||
|         for (const icon of json.titleIcons ?? defaultIcons) { |  | ||||||
|             if (icon === "defaults") { |             if (icon === "defaults") { | ||||||
|                 titleIcons.push(...defaultIcons); |                 titleIcons.push(...defaultIcons); | ||||||
|             } else { |             } else { | ||||||
|  | @ -264,37 +213,31 @@ export default class LayerConfig { | ||||||
| 
 | 
 | ||||||
|         this.titleIcons = trs(titleIcons, true); |         this.titleIcons = trs(titleIcons, true); | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         this.title = tr("title", undefined); |         this.title = tr("title", undefined); | ||||||
|         this.icon = tr("icon", ""); |         this.icon = tr("icon", ""); | ||||||
|         this.iconOverlays = (json.iconOverlays ?? []).map((overlay, i) => { |         this.iconOverlays = (json.iconOverlays ?? []).map((overlay, i) => { | ||||||
|             let tr = new TagRenderingConfig( |             let tr = new TagRenderingConfig(overlay.then, self.source.osmTags, `iconoverlays.${i}`); | ||||||
|                 overlay.then, |             if (typeof overlay.then === "string" && SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined) { | ||||||
|                 self.source.osmTags, |  | ||||||
|                 `iconoverlays.${i}` |  | ||||||
|             ); |  | ||||||
|             if ( |  | ||||||
|                 typeof overlay.then === "string" && |  | ||||||
|                 SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined |  | ||||||
|             ) { |  | ||||||
|                 tr = SharedTagRenderings.SharedIcons.get(overlay.then); |                 tr = SharedTagRenderings.SharedIcons.get(overlay.then); | ||||||
|             } |             } | ||||||
|             return { |             return { | ||||||
|                 if: FromJSON.Tag(overlay.if), |                 if: FromJSON.Tag(overlay.if), | ||||||
|                 then: tr, |                 then: tr, | ||||||
|                 badge: overlay.badge ?? false, |                 badge: overlay.badge ?? false | ||||||
|             }; |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt; |         const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt; | ||||||
|         if (iconPath.startsWith(Utils.assets_path)) { |         if (iconPath.startsWith(Utils.assets_path)) { | ||||||
|             const iconKey = iconPath.substr(Utils.assets_path.length); |             const iconKey = iconPath.substr(Utils.assets_path.length); | ||||||
|             if (Svg.All[iconKey] === undefined) { |             if (Svg.All[iconKey] === undefined) { | ||||||
|                 throw "Builtin SVG asset not found: " + iconPath; |                 throw "Builtin SVG asset not found: " + iconPath | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         this.isShown = tr("isShown", "yes"); |         this.isShown = tr("isShown", "yes"); | ||||||
|         this.iconSize = tr("iconSize", "40,40,center"); |         this.iconSize = tr("iconSize", "40,40,center"); | ||||||
|         this.label = tr("label", ""); |         this.label = tr("label", "") | ||||||
|         this.color = tr("color", "#0000ff"); |         this.color = tr("color", "#0000ff"); | ||||||
|         this.width = tr("width", "7"); |         this.width = tr("width", "7"); | ||||||
|         this.rotation = tr("rotation", "0"); |         this.rotation = tr("rotation", "0"); | ||||||
|  | @ -302,47 +245,42 @@ export default class LayerConfig { | ||||||
|          |          | ||||||
|         this.deletion = null; |         this.deletion = null; | ||||||
|         if(json.deletion === true){ |         if(json.deletion === true){ | ||||||
|             json.deletion = {}; |             json.deletion = { | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         if(json.deletion !== undefined && json.deletion !== false){ |         if(json.deletion !== undefined && json.deletion !== false){ | ||||||
|             this.deletion = new DeleteConfig(json.deletion, `${context}.deletion`); |             this.deletion = new DeleteConfig(json.deletion, `${context}.deletion`) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         if (json["showIf"] !== undefined) { |         if (json["showIf"] !== undefined) { | ||||||
|             throw ( |             throw "Invalid key on layerconfig " + this.id + ": showIf. Did you mean 'isShown' instead?"; | ||||||
|                 "Invalid key on layerconfig " + |  | ||||||
|                 this.id + |  | ||||||
|                 ": showIf. Did you mean 'isShown' instead?" |  | ||||||
|             ); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public CustomCodeSnippets(): string[] { |     public CustomCodeSnippets(): string[] { | ||||||
|         if (this.calculatedTags === undefined) { |         if (this.calculatedTags === undefined) { | ||||||
|             return []; |             return [] | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return this.calculatedTags.map((code) => code[1]); |         return this.calculatedTags.map(code => code[1]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public AddRoamingRenderings(addAll: { |     public AddRoamingRenderings(addAll: { | ||||||
|         tagRenderings: TagRenderingConfig[]; |         tagRenderings: TagRenderingConfig[], | ||||||
|         titleIcons: TagRenderingConfig[]; |         titleIcons: TagRenderingConfig[], | ||||||
|         iconOverlays: { |         iconOverlays: { "if": TagsFilter, then: TagRenderingConfig, badge: boolean }[] | ||||||
|             if: TagsFilter; | 
 | ||||||
|             then: TagRenderingConfig; |  | ||||||
|             badge: boolean; |  | ||||||
|         }[]; |  | ||||||
|     }): LayerConfig { |     }): LayerConfig { | ||||||
|         let insertionPoint = this.tagRenderings | 
 | ||||||
|             .map((tr) => tr.IsQuestionBoxElement()) |         let insertionPoint = this.tagRenderings.map(tr => tr.IsQuestionBoxElement()).indexOf(true) | ||||||
|             .indexOf(true); |  | ||||||
|         if (insertionPoint < 0) { |         if (insertionPoint < 0) { | ||||||
|             // No 'questions' defined - we just add them all to the end
 |             // No 'questions' defined - we just add them all to the end
 | ||||||
|             insertionPoint = this.tagRenderings.length; |             insertionPoint = this.tagRenderings.length; | ||||||
|         } |         } | ||||||
|         this.tagRenderings.splice(insertionPoint, 0, ...addAll.tagRenderings); |         this.tagRenderings.splice(insertionPoint, 0, ...addAll.tagRenderings); | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         this.iconOverlays.push(...addAll.iconOverlays); |         this.iconOverlays.push(...addAll.iconOverlays); | ||||||
|         for (const icon of addAll.titleIcons) { |         for (const icon of addAll.titleIcons) { | ||||||
|             this.titleIcons.splice(0, 0, icon); |             this.titleIcons.splice(0, 0, icon); | ||||||
|  | @ -351,42 +289,40 @@ export default class LayerConfig { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GetRoamingRenderings(): { |     public GetRoamingRenderings(): { | ||||||
|         tagRenderings: TagRenderingConfig[]; |         tagRenderings: TagRenderingConfig[], | ||||||
|         titleIcons: TagRenderingConfig[]; |         titleIcons: TagRenderingConfig[], | ||||||
|         iconOverlays: { |         iconOverlays: { "if": TagsFilter, then: TagRenderingConfig, badge: boolean }[] | ||||||
|             if: TagsFilter; | 
 | ||||||
|             then: TagRenderingConfig; |  | ||||||
|             badge: boolean; |  | ||||||
|         }[]; |  | ||||||
|     } { |     } { | ||||||
|         const tagRenderings = this.tagRenderings.filter((tr) => tr.roaming); | 
 | ||||||
|         const titleIcons = this.titleIcons.filter((tr) => tr.roaming); |         const tagRenderings = this.tagRenderings.filter(tr => tr.roaming); | ||||||
|         const iconOverlays = this.iconOverlays.filter((io) => io.then.roaming); |         const titleIcons = this.titleIcons.filter(tr => tr.roaming); | ||||||
|  |         const iconOverlays = this.iconOverlays.filter(io => io.then.roaming) | ||||||
| 
 | 
 | ||||||
|         return { |         return { | ||||||
|             tagRenderings: tagRenderings, |             tagRenderings: tagRenderings, | ||||||
|             titleIcons: titleIcons, |             titleIcons: titleIcons, | ||||||
|             iconOverlays: iconOverlays, |             iconOverlays: iconOverlays | ||||||
|         }; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     public GenerateLeafletStyle( |     } | ||||||
|         tags: UIEventSource<any>, | 
 | ||||||
|         clickable: boolean, |     public GenerateLeafletStyle(tags: UIEventSource<any>, clickable: boolean, widthHeight= "100%"): | ||||||
|         widthHeight = "100%" |         { | ||||||
|     ): { |             icon: | ||||||
|         icon: { |                 { | ||||||
|             html: BaseUIElement; |                     html: BaseUIElement, | ||||||
|             iconSize: [number, number]; |                     iconSize: [number, number], | ||||||
|             iconAnchor: [number, number]; |                     iconAnchor: [number, number], | ||||||
|             popupAnchor: [number, number]; |                     popupAnchor: [number, number], | ||||||
|             iconUrl: string; |                     iconUrl: string, | ||||||
|             className: string; |                     className: string | ||||||
|         }; |                 }, | ||||||
|         color: string; |             color: string, | ||||||
|         weight: number; |             weight: number, | ||||||
|         dashArray: number[]; |             dashArray: number[] | ||||||
|         } { |         } { | ||||||
|  | 
 | ||||||
|         function num(str, deflt = 40) { |         function num(str, deflt = 40) { | ||||||
|             const n = Number(str); |             const n = Number(str); | ||||||
|             if (isNaN(n)) { |             if (isNaN(n)) { | ||||||
|  | @ -405,7 +341,7 @@ export default class LayerConfig { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         function render(tr: TagRenderingConfig, deflt?: string) { |         function render(tr: TagRenderingConfig, deflt?: string) { | ||||||
|             const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt; |             const str = (tr?.GetRenderValue(tags.data)?.txt ?? deflt); | ||||||
|             return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, ""); |             return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, ""); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -414,16 +350,14 @@ export default class LayerConfig { | ||||||
|         let color = render(this.color, "#00f"); |         let color = render(this.color, "#00f"); | ||||||
| 
 | 
 | ||||||
|         if (color.startsWith("--")) { |         if (color.startsWith("--")) { | ||||||
|             color = getComputedStyle(document.body).getPropertyValue( |             color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color") | ||||||
|                 "--catch-detail-color" |  | ||||||
|             ); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const weight = rendernum(this.width, 5); |         const weight = rendernum(this.width, 5); | ||||||
| 
 | 
 | ||||||
|         const iconW = num(iconSize[0]); |         const iconW = num(iconSize[0]); | ||||||
|         let iconH = num(iconSize[1]); |         let iconH = num(iconSize[1]); | ||||||
|         const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center"; |         const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center" | ||||||
| 
 | 
 | ||||||
|         let anchorW = iconW / 2; |         let anchorW = iconW / 2; | ||||||
|         let anchorH = iconH / 2; |         let anchorH = iconH / 2; | ||||||
|  | @ -443,35 +377,31 @@ export default class LayerConfig { | ||||||
| 
 | 
 | ||||||
|         const iconUrlStatic = render(this.icon); |         const iconUrlStatic = render(this.icon); | ||||||
|         const self = this; |         const self = this; | ||||||
|         const mappedHtml = tags.map((tgs) => { |         const mappedHtml = tags.map(tgs => { | ||||||
|             function genHtmlFromString(sourcePart: string): BaseUIElement { |             function genHtmlFromString(sourcePart: string): BaseUIElement { | ||||||
|  | 
 | ||||||
|                 const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; |                 const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; | ||||||
|                 let html: BaseUIElement = new FixedUiElement( |                 let html: BaseUIElement = new FixedUiElement(`<img src="${sourcePart}" style="${style}" />`); | ||||||
|                     `<img src="${sourcePart}" style="${style}" />` |                 const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/) | ||||||
|                 ); |  | ||||||
|                 const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/); |  | ||||||
|                 if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { |                 if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { | ||||||
|                     html = new Combine([ |                     html = new Combine([ | ||||||
|                         (Svg.All[match[1] + ".svg"] as string).replace( |                         (Svg.All[match[1] + ".svg"] as string) | ||||||
|                             /#000000/g, |                             .replace(/#000000/g, match[2]) | ||||||
|                             match[2] |  | ||||||
|                         ), |  | ||||||
|                     ]).SetStyle(style); |                     ]).SetStyle(style); | ||||||
|                 } |                 } | ||||||
|                 return html; |                 return html; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|             // What do you mean, 'tgs' is never read?
 |             // What do you mean, 'tgs' is never read?
 | ||||||
|             // It is read implicitly in the 'render' method
 |             // It is read implicitly in the 'render' method
 | ||||||
|             const iconUrl = render(self.icon); |             const iconUrl = render(self.icon); | ||||||
|             const rotation = render(self.rotation, "0deg"); |             const rotation = render(self.rotation, "0deg"); | ||||||
| 
 | 
 | ||||||
|             let htmlParts: BaseUIElement[] = []; |             let htmlParts: BaseUIElement[] = []; | ||||||
|             let sourceParts = Utils.NoNull( |             let sourceParts = Utils.NoNull(iconUrl.split(";").filter(prt => prt != "")); | ||||||
|                 iconUrl.split(";").filter((prt) => prt != "") |  | ||||||
|             ); |  | ||||||
|             for (const sourcePart of sourceParts) { |             for (const sourcePart of sourceParts) { | ||||||
|                 htmlParts.push(genHtmlFromString(sourcePart)); |                 htmlParts.push(genHtmlFromString(sourcePart)) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             let badges = []; |             let badges = []; | ||||||
|  | @ -481,88 +411,79 @@ export default class LayerConfig { | ||||||
|                 } |                 } | ||||||
|                 if (iconOverlay.badge) { |                 if (iconOverlay.badge) { | ||||||
|                     const badgeParts: BaseUIElement[] = []; |                     const badgeParts: BaseUIElement[] = []; | ||||||
|                     const partDefs = iconOverlay.then |                     const partDefs = iconOverlay.then.GetRenderValue(tgs).txt.split(";").filter(prt => prt != ""); | ||||||
|                         .GetRenderValue(tgs) |  | ||||||
|                         .txt.split(";") |  | ||||||
|                         .filter((prt) => prt != ""); |  | ||||||
| 
 | 
 | ||||||
|                     for (const badgePartStr of partDefs) { |                     for (const badgePartStr of partDefs) { | ||||||
|                         badgeParts.push(genHtmlFromString(badgePartStr)); |                         badgeParts.push(genHtmlFromString(badgePartStr)) | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     const badgeCompound = new Combine(badgeParts).SetStyle( |                     const badgeCompound = new Combine(badgeParts) | ||||||
|                         "display:flex;position:relative;width:100%;height:100%;" |                         .SetStyle("display:flex;position:relative;width:100%;height:100%;"); | ||||||
|                     ); | 
 | ||||||
|  |                     badges.push(badgeCompound) | ||||||
| 
 | 
 | ||||||
|                     badges.push(badgeCompound); |  | ||||||
|                 } else { |                 } else { | ||||||
|                     htmlParts.push( |                     htmlParts.push(genHtmlFromString( | ||||||
|                         genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt) |                         iconOverlay.then.GetRenderValue(tgs).txt)); | ||||||
|                     ); |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (badges.length > 0) { |             if (badges.length > 0) { | ||||||
|                 const badgesComponent = new Combine(badges).SetStyle( |                 const badgesComponent = new Combine(badges) | ||||||
|                     "display:flex;height:50%;width:100%;position:absolute;top:50%;left:50%;" |                     .SetStyle("display:flex;height:50%;width:100%;position:absolute;top:50%;left:50%;"); | ||||||
|                 ); |                 htmlParts.push(badgesComponent) | ||||||
|                 htmlParts.push(badgesComponent); |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (sourceParts.length == 0) { |             if (sourceParts.length == 0) { | ||||||
|                 iconH = 0; |                 iconH = 0 | ||||||
|             } |             } | ||||||
|             try { |             try { | ||||||
|                 const label = self.label | 
 | ||||||
|                     ?.GetRenderValue(tgs) |                 const label = self.label?.GetRenderValue(tgs)?.Subs(tgs) | ||||||
|                     ?.Subs(tgs) |  | ||||||
|                     ?.SetClass("block text-center") |                     ?.SetClass("block text-center") | ||||||
|                     ?.SetStyle("margin-top: " + (iconH + 2) + "px"); |                     ?.SetStyle("margin-top: " + (iconH + 2) + "px") | ||||||
|                 if (label !== undefined) { |                 if (label !== undefined) { | ||||||
|                     htmlParts.push( |                     htmlParts.push(new Combine([label]).SetClass("flex flex-col items-center")) | ||||||
|                         new Combine([label]).SetClass("flex flex-col items-center") |  | ||||||
|                     ); |  | ||||||
|                 } |                 } | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 console.error(e, tgs); |                 console.error(e, tgs) | ||||||
|             } |             } | ||||||
|             return new Combine(htmlParts); |             return new Combine(htmlParts); | ||||||
|         }); |         }) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|         return { |         return { | ||||||
|             icon: { |             icon: | ||||||
|  |                 { | ||||||
|                     html: new VariableUiElement(mappedHtml), |                     html: new VariableUiElement(mappedHtml), | ||||||
|                     iconSize: [iconW, iconH], |                     iconSize: [iconW, iconH], | ||||||
|                     iconAnchor: [anchorW, anchorH], |                     iconAnchor: [anchorW, anchorH], | ||||||
|                     popupAnchor: [0, 3 - anchorH], |                     popupAnchor: [0, 3 - anchorH], | ||||||
|                     iconUrl: iconUrlStatic, |                     iconUrl: iconUrlStatic, | ||||||
|                 className: clickable |                     className: clickable ? "leaflet-div-icon" : "leaflet-div-icon unclickable" | ||||||
|                     ? "leaflet-div-icon" |  | ||||||
|                     : "leaflet-div-icon unclickable", |  | ||||||
|                 }, |                 }, | ||||||
|             color: color, |             color: color, | ||||||
|             weight: weight, |             weight: weight, | ||||||
|             dashArray: dashArray, |             dashArray: dashArray | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public ExtractImages(): Set<string> { |     public ExtractImages(): Set<string> { | ||||||
|         const parts: Set<string>[] = []; |         const parts: Set<string>[] = [] | ||||||
|         parts.push(...this.tagRenderings?.map((tr) => tr.ExtractImages(false))); |         parts.push(...this.tagRenderings?.map(tr => tr.ExtractImages(false))) | ||||||
|         parts.push(...this.titleIcons?.map((tr) => tr.ExtractImages(true))); |         parts.push(...this.titleIcons?.map(tr => tr.ExtractImages(true))) | ||||||
|         parts.push(this.icon?.ExtractImages(true)); |         parts.push(this.icon?.ExtractImages(true)) | ||||||
|         parts.push( |         parts.push(...this.iconOverlays?.map(overlay => overlay.then.ExtractImages(true))) | ||||||
|             ...this.iconOverlays?.map((overlay) => overlay.then.ExtractImages(true)) |  | ||||||
|         ); |  | ||||||
|         for (const preset of this.presets) { |         for (const preset of this.presets) { | ||||||
|             parts.push(new Set<string>(preset.description?.ExtractImages(false))); |             parts.push(new Set<string>(preset.description?.ExtractImages(false))) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const allIcons = new Set<string>(); |         const allIcons = new Set<string>(); | ||||||
|         for (const part of parts) { |         for (const part of parts) { | ||||||
|             part?.forEach(allIcons.add, allIcons); |             part?.forEach(allIcons.add, allIcons) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return allIcons; |         return allIcons; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; | import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; | ||||||
| import {AndOrTagConfigJson} from "./TagConfigJson"; | import {AndOrTagConfigJson} from "./TagConfigJson"; | ||||||
| import {DeleteConfigJson} from "./DeleteConfigJson"; | import {DeleteConfigJson} from "./DeleteConfigJson"; | ||||||
| import FilterConfigJson from "./FilterConfigJson"; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Configuration for a single layer |  * Configuration for a single layer | ||||||
|  | @ -218,16 +217,6 @@ export interface LayerConfigJson { | ||||||
|          * (The first sentence is until the first '.'-character in the description) |          * (The first sentence is until the first '.'-character in the description) | ||||||
|          */ |          */ | ||||||
|         description?: string | any, |         description?: string | any, | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * If set, the user will prompted to confirm the location before actually adding the data. |  | ||||||
|          * THis will be with a 'drag crosshair'-method. |  | ||||||
|          *  |  | ||||||
|          * If 'preferredBackgroundCategory' is set, the element will attempt to pick a background layer of that category. |  | ||||||
|          */ |  | ||||||
|         preciseInput?: true | { |  | ||||||
|             preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string |  | ||||||
|         } |  | ||||||
|     }[], |     }[], | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -244,12 +233,6 @@ export interface LayerConfigJson { | ||||||
|      */ |      */ | ||||||
|     tagRenderings?: (string | TagRenderingConfigJson) [], |     tagRenderings?: (string | TagRenderingConfigJson) [], | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * All the extra questions for filtering |  | ||||||
|      */ |  | ||||||
|     filter?: (FilterConfigJson) [], |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * This block defines under what circumstances the delete dialog is shown for objects of this layer. |      * This block defines under what circumstances the delete dialog is shown for objects of this layer. | ||||||
|      * If set, a dialog is shown to the user to (soft) delete the point. |      * If set, a dialog is shown to the user to (soft) delete the point. | ||||||
|  |  | ||||||
|  | @ -42,7 +42,6 @@ export default class LayoutConfig { | ||||||
|     public readonly enableGeolocation: boolean; |     public readonly enableGeolocation: boolean; | ||||||
|     public readonly enableBackgroundLayerSelection: boolean; |     public readonly enableBackgroundLayerSelection: boolean; | ||||||
|     public readonly enableShowAllQuestions: boolean; |     public readonly enableShowAllQuestions: boolean; | ||||||
|     public readonly enableExportButton: boolean; |  | ||||||
|     public readonly customCss?: string; |     public readonly customCss?: string; | ||||||
|     /* |     /* | ||||||
|     How long is the cache valid, in seconds? |     How long is the cache valid, in seconds? | ||||||
|  | @ -153,7 +152,6 @@ export default class LayoutConfig { | ||||||
|         this.enableAddNewPoints = json.enableAddNewPoints ?? true; |         this.enableAddNewPoints = json.enableAddNewPoints ?? true; | ||||||
|         this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true; |         this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true; | ||||||
|         this.enableShowAllQuestions = json.enableShowAllQuestions ?? false; |         this.enableShowAllQuestions = json.enableShowAllQuestions ?? false; | ||||||
|         this.enableExportButton = json.enableExportButton ?? false; |  | ||||||
|         this.customCss = json.customCss; |         this.customCss = json.customCss; | ||||||
|         this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) |         this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -15,7 +15,6 @@ import UnitConfigJson from "./UnitConfigJson"; | ||||||
|  * General remark: a type (string | any) indicates either a fixed or a translatable string. |  * General remark: a type (string | any) indicates either a fixed or a translatable string. | ||||||
|  */ |  */ | ||||||
| export interface LayoutConfigJson { | export interface LayoutConfigJson { | ||||||
|     |  | ||||||
|     /** |     /** | ||||||
|      * The id of this layout. |      * The id of this layout. | ||||||
|      * |      * | ||||||
|  | @ -227,10 +226,6 @@ export interface LayoutConfigJson { | ||||||
|      * Not only do we want to write consistent data to OSM, we also want to present this consistently to the user. |      * Not only do we want to write consistent data to OSM, we also want to present this consistently to the user. | ||||||
|      * This is handled by defining units. |      * This is handled by defining units. | ||||||
|      * |      * | ||||||
|      * # Rendering |  | ||||||
|      *  |  | ||||||
|      * To render a value with long (human) denomination, use {canonical(key)} |  | ||||||
|      * |  | ||||||
|      * # Usage |      * # Usage | ||||||
|      * |      * | ||||||
|      * First of all, you define which keys have units applied, for example: |      * First of all, you define which keys have units applied, for example: | ||||||
|  | @ -336,5 +331,4 @@ export interface LayoutConfigJson { | ||||||
|     enableGeolocation?: boolean; |     enableGeolocation?: boolean; | ||||||
|     enableBackgroundLayerSelection?: boolean; |     enableBackgroundLayerSelection?: boolean; | ||||||
|     enableShowAllQuestions?: boolean; |     enableShowAllQuestions?: boolean; | ||||||
|     enableExportButton?: boolean; |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -26,9 +26,6 @@ export default class TagRenderingConfig { | ||||||
|         readonly key: string, |         readonly key: string, | ||||||
|         readonly type: string, |         readonly type: string, | ||||||
|         readonly addExtraTags: TagsFilter[]; |         readonly addExtraTags: TagsFilter[]; | ||||||
|         readonly inline: boolean, |  | ||||||
|         readonly default?: string, |  | ||||||
|         readonly helperArgs?: (string | number | boolean)[] |  | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     readonly multiAnswer: boolean; |     readonly multiAnswer: boolean; | ||||||
|  | @ -76,9 +73,7 @@ export default class TagRenderingConfig { | ||||||
|                 type: json.freeform.type ?? "string", |                 type: json.freeform.type ?? "string", | ||||||
|                 addExtraTags: json.freeform.addExtraTags?.map((tg, i) => |                 addExtraTags: json.freeform.addExtraTags?.map((tg, i) => | ||||||
|                     FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [], |                     FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [], | ||||||
|                 inline: json.freeform.inline ?? false, | 
 | ||||||
|                 default: json.freeform.default, |  | ||||||
|                 helperArgs: json.freeform.helperArgs |  | ||||||
| 
 | 
 | ||||||
|             } |             } | ||||||
|             if (json.freeform["extraTags"] !== undefined) { |             if (json.freeform["extraTags"] !== undefined) { | ||||||
|  |  | ||||||
|  | @ -30,7 +30,6 @@ export interface TagRenderingConfigJson { | ||||||
|      * Allow freeform text input from the user |      * Allow freeform text input from the user | ||||||
|      */ |      */ | ||||||
|     freeform?: { |     freeform?: { | ||||||
|      |  | ||||||
|         /** |         /** | ||||||
|          * If this key is present, then 'render' is used to display the value. |          * If this key is present, then 'render' is used to display the value. | ||||||
|          * If this is undefined, the rendering is _always_ shown |          * If this is undefined, the rendering is _always_ shown | ||||||
|  | @ -41,30 +40,13 @@ export interface TagRenderingConfigJson { | ||||||
|          * See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values |          * See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values | ||||||
|          */ |          */ | ||||||
|         type?: string, |         type?: string, | ||||||
|         /** |  | ||||||
|          * Extra parameters to initialize the input helper arguments. |  | ||||||
|          * For semantics, see the 'SpecialInputElements.md' |  | ||||||
|          */ |  | ||||||
|         helperArgs?: (string | number | boolean)[]; |  | ||||||
|         /** |         /** | ||||||
|          * If a value is added with the textfield, these extra tag is addded. |          * If a value is added with the textfield, these extra tag is addded. | ||||||
|          * Useful to add a 'fixme=freeform textfield used - to be checked' |          * Useful to add a 'fixme=freeform textfield used - to be checked' | ||||||
|          **/ |          **/ | ||||||
|         addExtraTags?: string[]; |         addExtraTags?: string[]; | ||||||
| 
 | 
 | ||||||
|         /** |  | ||||||
|          * When set, influences the way a question is asked. |  | ||||||
|          * Instead of showing a full-widht text field, the text field will be shown within the rendering of the question. |  | ||||||
|          *  |  | ||||||
|          * This combines badly with special input elements, as it'll distort the layout. |  | ||||||
|          */ |  | ||||||
|         inline?: boolean |  | ||||||
|          |          | ||||||
|         /** |  | ||||||
|          * default value to enter if no previous tagging is present. |  | ||||||
|          * Normally undefined (aka do not enter anything) |  | ||||||
|          */ |  | ||||||
|         default?: string |  | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -18,9 +18,9 @@ | ||||||
|  Development |  Development | ||||||
|  ----------- |  ----------- | ||||||
|   |   | ||||||
|  **Windows users**: All scripts are made for linux devices. Use the Ubuntu terminal for Windows (or even better - make the switch ;) ). If you are using Visual Studio Code you can use a [WSL Remote](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) window, or use the Devcontainer (see more details later). |  **Windows users**: All scripts are made for linux devices. Use the Ubuntu terminal for Windows (or even better - make the switch ;) ). If you are using Visual Studio, open everything in a 'new WSL Window'. | ||||||
|   |   | ||||||
|  To develop and build MapComplete, you |  To develop and build MapComplete, yo | ||||||
|   |   | ||||||
| 0. Make sure you have a recent version of nodejs - at least 12.0, preferably 15 | 0. Make sure you have a recent version of nodejs - at least 12.0, preferably 15 | ||||||
| 0. Make a fork and clone the repository. | 0. Make a fork and clone the repository. | ||||||
|  | @ -29,30 +29,6 @@ | ||||||
| 4. Run `npm run start` to host a local testversion at http://localhost:1234/index.html | 4. Run `npm run start` to host a local testversion at http://localhost:1234/index.html | ||||||
| 5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version. | 5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version. | ||||||
| 
 | 
 | ||||||
|  Development using Windows |  | ||||||
|  ------------------------ |  | ||||||
| 
 |  | ||||||
|  For Windows you can use the devcontainer, or the WSL subsystem. |  | ||||||
| 
 |  | ||||||
|  To use the devcontainer in Visual Studio Code: |  | ||||||
| 
 |  | ||||||
| 0. Make sure you have installed the [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension and it's dependencies. |  | ||||||
| 1. Make a fork and clone the repository. |  | ||||||
| 2. After cloning, Visual Studio Code will ask you if you want to use the devcontainer. |  | ||||||
| 3. Then you can either clone it again in a volume (for better performance), or open the current folder in a container. |  | ||||||
| 4. By now, you should be able to run `npm run start` to host a local testversion at http://localhost:1234/index.html |  | ||||||
| 5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version. |  | ||||||
| 
 |  | ||||||
|  To use the WSL in Visual Studio Code: |  | ||||||
| 
 |  | ||||||
| 0. Make sure you have installed the [Remote - WSL]() extension and it's dependencies. |  | ||||||
| 1. Open a remote WSL window using the button in the bottom left. |  | ||||||
| 2. Make a fork and clone the repository. |  | ||||||
| 3. Install `npm` using `sudo apt install npm`. |  | ||||||
| 4. Run `npm run init` and generate some additional dependencies and generated files. Note that it'll install the dependencies too |  | ||||||
| 5. Run `npm run start` to host a local testversion at http://localhost:1234/index.html |  | ||||||
| 6. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version. |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|  Automatic deployment |  Automatic deployment | ||||||
|  -------------------- |  -------------------- | ||||||
|  |  | ||||||
|  | @ -20,34 +20,6 @@ the URL-parameters are stated in the part between the `?` and the `#`. There are | ||||||
| Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case. | Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| backend |  | ||||||
| --------- |  | ||||||
| 
 |  | ||||||
| The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_ |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| test |  | ||||||
| ------ |  | ||||||
| 
 |  | ||||||
| If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org The default value is _false_ |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| layout |  | ||||||
| -------- |  | ||||||
| 
 |  | ||||||
| The layout to load into MapComplete The default value is __ |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| userlayout |  | ||||||
| ------------ |  | ||||||
| 
 |  | ||||||
| If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: |  | ||||||
| 
 |  | ||||||
| - The hash of the URL contains a base64-encoded .json-file containing the theme definition |  | ||||||
| - The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator |  | ||||||
| - The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme The default value is _false_ |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|  layer-control-toggle  |  layer-control-toggle  | ||||||
| ---------------------- | ---------------------- | ||||||
| 
 | 
 | ||||||
|  | @ -63,19 +35,19 @@ The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 | ||||||
|  z  |  z  | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
| The initial/current zoom level The default value is _14_ |  The initial/current zoom level The default value is _0_ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  lat  |  lat  | ||||||
| ----- | ----- | ||||||
| 
 | 
 | ||||||
| The initial/current latitude The default value is _51.2095_ |  The initial/current latitude The default value is _0_ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  lon  |  lon  | ||||||
| ----- | ----- | ||||||
| 
 | 
 | ||||||
| The initial/current longitude of the app The default value is _3.2228_ |  The initial/current longitude of the app The default value is _0_ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  fs-userbadge  |  fs-userbadge  | ||||||
|  | @ -138,16 +110,10 @@ fs-all-questions | ||||||
|  Always show all questions The default value is _false_ |  Always show all questions The default value is _false_ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| fs-export |  test  | ||||||
| ----------- | ------ | ||||||
| 
 | 
 | ||||||
| If set, enables the 'download'-button to download everything as geojson The default value is _false_ |  If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org The default value is _false_ | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| fake-user |  | ||||||
| ----------- |  | ||||||
| 
 |  | ||||||
| If true, 'dryrun' mode is activated and a fake user account is loaded The default value is _false_ |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  debug  |  debug  | ||||||
|  | @ -156,6 +122,12 @@ debug | ||||||
|  If true, shows some extra debugging help such as all the available tags on every object The default value is _false_ |  If true, shows some extra debugging help such as all the available tags on every object The default value is _false_ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  backend  | ||||||
|  | --------- | ||||||
|  | 
 | ||||||
|  |  The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  custom-css  |  custom-css  | ||||||
| ------------ | ------------ | ||||||
| 
 | 
 | ||||||
|  | @ -168,10 +140,6 @@ background | ||||||
|  The id of the background layer to start with The default value is _osm_ |  The id of the background layer to start with The default value is _osm_ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| oauth_token |  | ||||||
| ------------- |  | ||||||
| 
 |  | ||||||
| Used to complete the login No default value set |  | ||||||
|  layer-<layer-id>  |  layer-<layer-id>  | ||||||
| ------------------ | ------------------ | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -41,8 +41,6 @@ import FeatureSource from "./Logic/FeatureSource/FeatureSource"; | ||||||
| import AllKnownLayers from "./Customizations/AllKnownLayers"; | import AllKnownLayers from "./Customizations/AllKnownLayers"; | ||||||
| import LayerConfig from "./Customizations/JSON/LayerConfig"; | import LayerConfig from "./Customizations/JSON/LayerConfig"; | ||||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; | import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; | ||||||
| import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter"; |  | ||||||
| import jsPDF from "jspdf"; |  | ||||||
| import FilterView from "./UI/BigComponents/FilterView"; | import FilterView from "./UI/BigComponents/FilterView"; | ||||||
| 
 | 
 | ||||||
| export class InitUiElements { | export class InitUiElements { | ||||||
|  | @ -59,8 +57,7 @@ export class InitUiElements { | ||||||
|         `Error: incorrect layout <i>${layoutName}</i><br/><a href='https://${window.location.host}/'>Go back</a>` |         `Error: incorrect layout <i>${layoutName}</i><br/><a href='https://${window.location.host}/'>Go back</a>` | ||||||
|       ) |       ) | ||||||
|         .AttachTo("centermessage") |         .AttachTo("centermessage") | ||||||
|                 .onClick(() => { |         .onClick(() => {}); | ||||||
|                 }); |  | ||||||
|       throw "Incorrect layout"; |       throw "Incorrect layout"; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -222,34 +219,8 @@ export class InitUiElements { | ||||||
|       State.state.locationControl.ping(); |       State.state.locationControl.ping(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         // To download pdf of leaflet you need to turn it into and image first
 |  | ||||||
|         // Then export that image as a pdf
 |  | ||||||
|         // leaflet-simple-map-screenshoter: to make image
 |  | ||||||
|         // jsPDF:                           to make pdf
 |  | ||||||
| 
 |  | ||||||
|         const screenshot = new MapControlButton( |  | ||||||
|             new CenterFlexedElement( |  | ||||||
|                 Img.AsImageElement(Svg.bug, "", "width:1.25rem;height:1.25rem") |  | ||||||
|             ) |  | ||||||
|         ).onClick(() => { |  | ||||||
|             const screenshotter = new SimpleMapScreenshoter(); |  | ||||||
|             console.log("Debug - Screenshot"); |  | ||||||
|             screenshotter.addTo(State.state.leafletMap.data); |  | ||||||
|             let doc = new jsPDF(); |  | ||||||
|             screenshotter.takeScreen('image').then(image => { |  | ||||||
|                 // TO DO: scale image on pdf to its original size
 |  | ||||||
|                 doc.addImage(image, 'PNG', 0, 0, screen.width / 10, screen.height / 10); |  | ||||||
|                 doc.setDisplayMode('fullheight'); |  | ||||||
|                 doc.save("Screenshot"); |  | ||||||
|             }); |  | ||||||
|             //screenshotter.remove();
 |  | ||||||
|             // The line below is for downloading the png
 |  | ||||||
|             //screenshotter.takeScreen().then(blob => Utils.offerContentsAsDownloadableFile(blob, "Screenshot.png"));
 |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|     new Combine( |     new Combine( | ||||||
|             [plus, min, geolocationButton, screenshot].map((el) => el.SetClass("m-0.5 md:m-1")) |       [plus, min, geolocationButton].map((el) => el.SetClass("m-0.5 md:m-1")) | ||||||
|     ) |     ) | ||||||
|       .SetClass("flex flex-col") |       .SetClass("flex flex-col") | ||||||
|       .AttachTo("bottom-right"); |       .AttachTo("bottom-right"); | ||||||
|  | @ -435,7 +406,10 @@ export class InitUiElements { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private static InitBaseMap() { |   private static InitBaseMap() { | ||||||
|         State.state.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(State.state.locationControl); |     State.state.availableBackgroundLayers = new AvailableBaseLayers( | ||||||
|  |       State.state.locationControl | ||||||
|  |     ).availableEditorLayers; | ||||||
|  | 
 | ||||||
|     State.state.backgroundLayer = State.state.backgroundLayerId.map( |     State.state.backgroundLayer = State.state.backgroundLayerId.map( | ||||||
|       (selectedId: string) => { |       (selectedId: string) => { | ||||||
|         if (selectedId === undefined) { |         if (selectedId === undefined) { | ||||||
|  | @ -542,7 +516,6 @@ export class InitUiElements { | ||||||
|       state.selectedElement |       state.selectedElement | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|         State.state.featurePipeline = source; |  | ||||||
|     new ShowDataLayer( |     new ShowDataLayer( | ||||||
|       source.features, |       source.features, | ||||||
|       State.state.leafletMap, |       State.state.leafletMap, | ||||||
|  |  | ||||||
|  | @ -1,12 +1,11 @@ | ||||||
| import * as editorlayerindex from "../../assets/editor-layer-index.json" | import * as editorlayerindex from "../../assets/editor-layer-index.json" | ||||||
| import BaseLayer from "../../Models/BaseLayer"; | import BaseLayer from "../../Models/BaseLayer"; | ||||||
| import * as L from "leaflet"; | import * as L from "leaflet"; | ||||||
| import {TileLayer} from "leaflet"; |  | ||||||
| import * as X from "leaflet-providers"; | import * as X from "leaflet-providers"; | ||||||
| import {UIEventSource} from "../UIEventSource"; | import {UIEventSource} from "../UIEventSource"; | ||||||
| import {GeoOperations} from "../GeoOperations"; | import {GeoOperations} from "../GeoOperations"; | ||||||
|  | import {TileLayer} from "leaflet"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
| import Loc from "../../Models/Loc"; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Calculates which layers are available at the current location |  * Calculates which layers are available at the current location | ||||||
|  | @ -25,23 +24,25 @@ export default class AvailableBaseLayers { | ||||||
|                 false, false), |                 false, false), | ||||||
|             feature: null, |             feature: null, | ||||||
|             max_zoom: 19, |             max_zoom: 19, | ||||||
|             min_zoom: 0, |             min_zoom: 0 | ||||||
|             isBest: false, // This is a lie! Of course OSM is the best map! (But not in this context)
 |  | ||||||
|             category: "osmbasedmap" |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex()); |  | ||||||
| 
 | 
 | ||||||
|     public static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> { |     public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex()); | ||||||
|         const source = location.map( |     public availableEditorLayers: UIEventSource<BaseLayer[]>; | ||||||
|  | 
 | ||||||
|  |     constructor(location: UIEventSource<{ lat: number, lon: number, zoom: number }>) { | ||||||
|  |         const self = this; | ||||||
|  |         this.availableEditorLayers = | ||||||
|  |             location.map( | ||||||
|                 (currentLocation) => { |                 (currentLocation) => { | ||||||
| 
 | 
 | ||||||
|                     if (currentLocation === undefined) { |                     if (currentLocation === undefined) { | ||||||
|                         return AvailableBaseLayers.layerOverview; |                         return AvailableBaseLayers.layerOverview; | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                 const currentLayers = source?.data; // A bit unorthodox - I know
 |                     const currentLayers = self.availableEditorLayers?.data; | ||||||
|                 const newLayers = AvailableBaseLayers.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat); |                     const newLayers = AvailableBaseLayers.AvailableLayersAt(currentLocation?.lon, currentLocation?.lat); | ||||||
| 
 | 
 | ||||||
|                     if (currentLayers === undefined) { |                     if (currentLayers === undefined) { | ||||||
|                         return newLayers; |                         return newLayers; | ||||||
|  | @ -57,55 +58,11 @@ export default class AvailableBaseLayers { | ||||||
| 
 | 
 | ||||||
|                     return currentLayers; |                     return currentLayers; | ||||||
|                 }); |                 }); | ||||||
|         return source; | 
 | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> { |     private static AvailableLayersAt(lon: number, lat: number): BaseLayer[] { | ||||||
|         return AvailableBaseLayers.AvailableLayersAt(location).map(available => { |  | ||||||
|             // First float all 'best layers' to the top
 |  | ||||||
|             available.sort((a, b) => { |  | ||||||
|                     if (a.isBest && b.isBest) { |  | ||||||
|                         return 0; |  | ||||||
|                     } |  | ||||||
|                     if (!a.isBest) { |  | ||||||
|                         return 1 |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     return -1; |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|             if (preferedCategory.data === undefined) { |  | ||||||
|                 return available[0] |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             let prefered: string [] |  | ||||||
|             if (typeof preferedCategory.data === "string") { |  | ||||||
|                 prefered = [preferedCategory.data] |  | ||||||
|             } else { |  | ||||||
|                 prefered = preferedCategory.data; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             prefered.reverse(); |  | ||||||
|             for (const category of prefered) { |  | ||||||
|                 //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
 |  | ||||||
|                 available.sort((a, b) => { |  | ||||||
|                         if (a.category === category && b.category === category) { |  | ||||||
|                             return 0; |  | ||||||
|                         } |  | ||||||
|                         if (a.category !== category) { |  | ||||||
|                             return 1 |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         return -1; |  | ||||||
|                     } |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|             return available[0] |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] { |  | ||||||
|         const availableLayers = [AvailableBaseLayers.osmCarto] |         const availableLayers = [AvailableBaseLayers.osmCarto] | ||||||
|         const globalLayers = []; |         const globalLayers = []; | ||||||
|         for (const layerOverviewItem of AvailableBaseLayers.layerOverview) { |         for (const layerOverviewItem of AvailableBaseLayers.layerOverview) { | ||||||
|  | @ -183,9 +140,7 @@ export default class AvailableBaseLayers { | ||||||
|                 min_zoom: props.min_zoom ?? 1, |                 min_zoom: props.min_zoom ?? 1, | ||||||
|                 name: props.name, |                 name: props.name, | ||||||
|                 layer: leafletLayer, |                 layer: leafletLayer, | ||||||
|                 feature: layer, |                 feature: layer | ||||||
|                 isBest: props.best ?? false, |  | ||||||
|                 category: props.category |  | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|         return layers; |         return layers; | ||||||
|  | @ -197,16 +152,15 @@ export default class AvailableBaseLayers { | ||||||
|         function l(id: string, name: string): BaseLayer { |         function l(id: string, name: string): BaseLayer { | ||||||
|             try { |             try { | ||||||
|                 const layer: any = () => L.tileLayer.provider(id, undefined); |                 const layer: any = () => L.tileLayer.provider(id, undefined); | ||||||
|                 return { |                 const baseLayer: BaseLayer = { | ||||||
|                     feature: null, |                     feature: null, | ||||||
|                     id: id, |                     id: id, | ||||||
|                     name: name, |                     name: name, | ||||||
|                     layer: layer, |                     layer: layer, | ||||||
|                     min_zoom: layer.minzoom, |                     min_zoom: layer.minzoom, | ||||||
|                     max_zoom: layer.maxzoom, |                     max_zoom: layer.maxzoom | ||||||
|                     category: "osmbasedmap", |  | ||||||
|                     isBest: false |  | ||||||
|                 } |                 } | ||||||
|  |                 return baseLayer | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 console.error("Could not find provided layer", name, e); |                 console.error("Could not find provided layer", name, e); | ||||||
|                 return null; |                 return null; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import * as L from "leaflet"; | import * as L from "leaflet"; | ||||||
| import { UIEventSource } from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource"; | ||||||
|  | import { Utils } from "../../Utils"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import Img from "../../UI/Base/Img"; | import Img from "../../UI/Base/Img"; | ||||||
| import { LocalStorageSource } from "../Web/LocalStorageSource"; | import { LocalStorageSource } from "../Web/LocalStorageSource"; | ||||||
|  | @ -14,19 +15,11 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|    */ |    */ | ||||||
|   private readonly _isActive: UIEventSource<boolean>; |   private readonly _isActive: UIEventSource<boolean>; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user |  | ||||||
|      * @private |  | ||||||
|      */ |  | ||||||
|     private readonly _isLocked: UIEventSource<boolean>; |  | ||||||
| 
 |  | ||||||
|   /** |   /** | ||||||
|    * The callback over the permission API |    * The callback over the permission API | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   private readonly _permission: UIEventSource<string>; |   private readonly _permission: UIEventSource<string>; | ||||||
| 
 |  | ||||||
|   /*** |   /*** | ||||||
|    * The marker on the map, in order to update it |    * The marker on the map, in order to update it | ||||||
|    * @private |    * @private | ||||||
|  | @ -46,15 +39,11 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   private readonly _leafletMap: UIEventSource<L.Map>; |   private readonly _leafletMap: UIEventSource<L.Map>; | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   /** |   /** | ||||||
|    * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs |    * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   private _lastUserRequest: Date; |   private _lastUserRequest: Date; | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   /** |   /** | ||||||
|    * A small flag on localstorage. If the user previously granted the geolocation, it will be set. |    * A small flag on localstorage. If the user previously granted the geolocation, it will be set. | ||||||
|    * On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions. |    * On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions. | ||||||
|  | @ -78,32 +67,28 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|       "geolocation-permissions" |       "geolocation-permissions" | ||||||
|     ); |     ); | ||||||
|     const isActive = new UIEventSource<boolean>(false); |     const isActive = new UIEventSource<boolean>(false); | ||||||
|         const isLocked = new UIEventSource<boolean>(false); | 
 | ||||||
|     super( |     super( | ||||||
|       hasLocation.map( |       hasLocation.map( | ||||||
|                 (hasLocationData) => { |         (hasLocation) => { | ||||||
|                     let icon: string; |           if (hasLocation) { | ||||||
| 
 |  | ||||||
|                     if (isLocked.data) { |  | ||||||
|                         icon = Svg.crosshair_locked; |  | ||||||
|                     } else if (hasLocationData) { |  | ||||||
|                         icon = Svg.crosshair_blue; |  | ||||||
|                     } else if (isActive.data) { |  | ||||||
|                         icon = Svg.crosshair_blue_center; |  | ||||||
|                     } else { |  | ||||||
|                         icon = Svg.crosshair; |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|             return new CenterFlexedElement( |             return new CenterFlexedElement( | ||||||
|                         Img.AsImageElement(icon, "", "width:1.25rem;height:1.25rem") |               Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem") | ||||||
|                     ); |             ); // crosshair_blue_ui()
 | ||||||
| 
 |           } | ||||||
|  |           if (isActive.data) { | ||||||
|  |             return new CenterFlexedElement( | ||||||
|  |               Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem") | ||||||
|  |             ); // crosshair_blue_center_ui
 | ||||||
|  |           } | ||||||
|  |           return new CenterFlexedElement( | ||||||
|  |             Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem") | ||||||
|  |           ); //crosshair_ui
 | ||||||
|         }, |         }, | ||||||
|                 [isActive, isLocked] |         [isActive] | ||||||
|       ) |       ) | ||||||
|     ); |     ); | ||||||
|     this._isActive = isActive; |     this._isActive = isActive; | ||||||
|         this._isLocked = isLocked; |  | ||||||
|     this._permission = new UIEventSource<string>(""); |     this._permission = new UIEventSource<string>(""); | ||||||
|     this._previousLocationGrant = previousLocationGrant; |     this._previousLocationGrant = previousLocationGrant; | ||||||
|     this._currentGPSLocation = currentGPSLocation; |     this._currentGPSLocation = currentGPSLocation; | ||||||
|  | @ -125,14 +110,13 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|       self.SetClass(pointerClass); |       self.SetClass(pointerClass); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|         this.onClick(() => { |     this.onClick(() => self.init(true)); | ||||||
|             if (self._hasLocation.data) { |  | ||||||
|                 self._isLocked.setData(!self._isLocked.data); |  | ||||||
|             } |  | ||||||
|             self.init(true); |  | ||||||
|         }); |  | ||||||
|     this.init(false); |     this.init(false); | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|  |   private init(askPermission: boolean) { | ||||||
|  |     const self = this; | ||||||
|  |     const map = this._leafletMap.data; | ||||||
| 
 | 
 | ||||||
|     this._currentGPSLocation.addCallback((location) => { |     this._currentGPSLocation.addCallback((location) => { | ||||||
|       self._previousLocationGrant.setData("granted"); |       self._previousLocationGrant.setData("granted"); | ||||||
|  | @ -141,8 +125,6 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|         (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000; |         (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000; | ||||||
|       if (timeSinceRequest < 30) { |       if (timeSinceRequest < 30) { | ||||||
|         self.MoveToCurrentLoction(16); |         self.MoveToCurrentLoction(16); | ||||||
|             } else if (self._isLocked.data) { |  | ||||||
|                 self.MoveToCurrentLoction(); |  | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       let color = "#1111cc"; |       let color = "#1111cc"; | ||||||
|  | @ -159,8 +141,6 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|         iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
 |         iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
 | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|             const map = self._leafletMap.data; |  | ||||||
| 
 |  | ||||||
|       const newMarker = L.marker(location.latlng, { icon: icon }); |       const newMarker = L.marker(location.latlng, { icon: icon }); | ||||||
|       newMarker.addTo(map); |       newMarker.addTo(map); | ||||||
| 
 | 
 | ||||||
|  | @ -169,14 +149,7 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|       } |       } | ||||||
|       self._marker = newMarker; |       self._marker = newMarker; | ||||||
|     }); |     }); | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     private init(askPermission: boolean) { |  | ||||||
|         const self = this; |  | ||||||
|         if (self._isActive.data) { |  | ||||||
|             self.MoveToCurrentLoction(16); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|     try { |     try { | ||||||
|       navigator?.permissions |       navigator?.permissions | ||||||
|         ?.query({ name: "geolocation" }) |         ?.query({ name: "geolocation" }) | ||||||
|  | @ -201,6 +174,31 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private locate() { | ||||||
|  |     const self = this; | ||||||
|  |     const map: any = this._leafletMap.data; | ||||||
|  | 
 | ||||||
|  |     if (navigator.geolocation) { | ||||||
|  |       navigator.geolocation.getCurrentPosition( | ||||||
|  |         function (position) { | ||||||
|  |           self._currentGPSLocation.setData({ | ||||||
|  |             latlng: [position.coords.latitude, position.coords.longitude], | ||||||
|  |             accuracy: position.coords.accuracy, | ||||||
|  |           }); | ||||||
|  |         }, | ||||||
|  |         function () { | ||||||
|  |           console.warn("Could not get location with navigator.geolocation"); | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |       return; | ||||||
|  |     } else { | ||||||
|  |       map.findAccuratePosition({ | ||||||
|  |         maxWait: 10000, // defaults to 10000
 | ||||||
|  |         desiredAccuracy: 50, // defaults to 20
 | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private MoveToCurrentLoction(targetZoom = 16) { |   private MoveToCurrentLoction(targetZoom = 16) { | ||||||
|     const location = this._currentGPSLocation.data; |     const location = this._currentGPSLocation.data; | ||||||
|     this._lastUserRequest = undefined; |     this._lastUserRequest = undefined; | ||||||
|  | @ -251,21 +249,17 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     console.log("Searching location using GPS"); |     console.log("Searching location using GPS"); | ||||||
|  |     this.locate(); | ||||||
| 
 | 
 | ||||||
|         if (self._isActive.data) { |     if (!self._isActive.data) { | ||||||
|  |       self._isActive.setData(true); | ||||||
|  |       Utils.DoEvery(60000, () => { | ||||||
|  |         if (document.visibilityState !== "visible") { | ||||||
|  |           console.log("Not starting gps: document not visible"); | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|         self._isActive.setData(true); |         this.locate(); | ||||||
|         navigator.geolocation.watchPosition( |  | ||||||
|             function (position) { |  | ||||||
|                 self._currentGPSLocation.setData({ |  | ||||||
|                     latlng: [position.coords.latitude, position.coords.longitude], |  | ||||||
|                     accuracy: position.coords.accuracy, |  | ||||||
|       }); |       }); | ||||||
|             }, |     } | ||||||
|             function () { |  | ||||||
|                 console.warn("Could not get location with navigator.geolocation"); |  | ||||||
|             } |  | ||||||
|         ); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -47,12 +47,7 @@ export default class StrayClickHandler { | ||||||
|                     popupAnchor: [0, -45] |                     popupAnchor: [0, -45] | ||||||
|                 }) |                 }) | ||||||
|             }); |             }); | ||||||
|             const popup = L.popup({ |             const popup = L.popup().setContent("<div id='strayclick'></div>"); | ||||||
|                 autoPan: true, |  | ||||||
|                 autoPanPaddingTopLeft: [15,15], |  | ||||||
|                 closeOnEscapeKey: true, |  | ||||||
|                 autoClose: true |  | ||||||
|             }).setContent("<div id='strayclick' style='height: 65vh'></div>"); |  | ||||||
|             self._lastMarker.addTo(leafletMap.data); |             self._lastMarker.addTo(leafletMap.data); | ||||||
|             self._lastMarker.bindPopup(popup); |             self._lastMarker.bindPopup(popup); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -46,10 +46,7 @@ export default class FeaturePipeline implements FeatureSource { | ||||||
|         const geojsonSources: FeatureSource [] = GeoJsonSource |         const geojsonSources: FeatureSource [] = GeoJsonSource | ||||||
|             .ConstructMultiSource(flayers.data, locationControl) |             .ConstructMultiSource(flayers.data, locationControl) | ||||||
|             .map(geojsonSource => { |             .map(geojsonSource => { | ||||||
|                 let source = new RegisteringFeatureSource( |                 let source = new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource)); | ||||||
|                     new FeatureDuplicatorPerLayer(flayers, |  | ||||||
|                             geojsonSource |  | ||||||
|                     )); |  | ||||||
|                 if(!geojsonSource.isOsmCache){ |                 if(!geojsonSource.isOsmCache){ | ||||||
|                     source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features); |                     source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import {UIEventSource} from "../UIEventSource"; | import {UIEventSource} from "../UIEventSource"; | ||||||
| import {Utils} from "../../Utils"; |  | ||||||
| 
 | 
 | ||||||
| export default interface FeatureSource { | export default interface FeatureSource { | ||||||
|     features: UIEventSource<{feature: any, freshness: Date}[]>; |     features: UIEventSource<{feature: any, freshness: Date}[]>; | ||||||
|  | @ -8,38 +7,3 @@ export default interface FeatureSource { | ||||||
|      */ |      */ | ||||||
|     name: string; |     name: string; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| export class FeatureSourceUtils { |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Exports given featurePipeline as a geojson FeatureLists (downloads as a json) |  | ||||||
|      * @param featurePipeline The FeaturePipeline you want to export |  | ||||||
|      * @param options The options object |  | ||||||
|      * @param options.metadata True if you want to include the MapComplete metadata, false otherwise |  | ||||||
|      */ |  | ||||||
|     public static extractGeoJson(featurePipeline: FeatureSource, options: { metadata?: boolean } = {}) { |  | ||||||
|         let defaults = { |  | ||||||
|             metadata: false, |  | ||||||
|         } |  | ||||||
|         options = Utils.setDefaults(options, defaults); |  | ||||||
| 
 |  | ||||||
|         // Select all features, ignore the freshness and other data
 |  | ||||||
|         let featureList: any[] = featurePipeline.features.data.map((feature) => feature.feature); |  | ||||||
| 
 |  | ||||||
|         if (!options.metadata) { |  | ||||||
|             for (let i = 0; i < featureList.length; i++) { |  | ||||||
|                 let feature = featureList[i]; |  | ||||||
|                 for (let property in feature.properties) { |  | ||||||
|                     if (property[0] == "_") { |  | ||||||
|                         delete featureList[i]["properties"][property]; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return {type: "FeatureCollection", features: featureList} |  | ||||||
| 
 |  | ||||||
|    |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | @ -175,7 +175,7 @@ export default class GeoJsonSource implements FeatureSource { | ||||||
| 
 | 
 | ||||||
|                 let freshness: Date = time; |                 let freshness: Date = time; | ||||||
|                 if (feature.properties["_last_edit:timestamp"] !== undefined) { |                 if (feature.properties["_last_edit:timestamp"] !== undefined) { | ||||||
|                     freshness = new Date(feature.properties["_last_edit:timestamp"]) |                     freshness = new Date(feature["_last_edit:timestamp"]) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 newFeatures.push({feature: feature, freshness: freshness}) |                 newFeatures.push({feature: feature, freshness: freshness}) | ||||||
|  |  | ||||||
|  | @ -6,14 +6,11 @@ export class GeoOperations { | ||||||
|         return turf.area(feature); |         return turf.area(feature); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * Converts a GeoJSon feature to a point feature |  | ||||||
|      * @param feature |  | ||||||
|      */ |  | ||||||
|     static centerpoint(feature: any) { |     static centerpoint(feature: any) { | ||||||
|         const newFeature = turf.center(feature); |         const newFeature = turf.center(feature); | ||||||
|         newFeature.properties = feature.properties; |         newFeature.properties = feature.properties; | ||||||
|         newFeature.id = feature.id; |         newFeature.id = feature.id; | ||||||
|  | 
 | ||||||
|         return newFeature; |         return newFeature; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -276,14 +273,6 @@ export class GeoOperations { | ||||||
|         } |         } | ||||||
|         return undefined; |         return undefined; | ||||||
|     } |     } | ||||||
|     /** |  | ||||||
|      * Generates the closest point on a way from a given point |  | ||||||
|      * @param way The road on which you want to find a point |  | ||||||
|      * @param point Point defined as [lon, lat] |  | ||||||
|      */ |  | ||||||
|     public static nearestPoint(way, point: [number, number]){ |  | ||||||
|         return turf.nearestPointOnLine(way, point, {units: "kilometers"}); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,8 +6,6 @@ import Constants from "../../Models/Constants"; | ||||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | import FeatureSource from "../FeatureSource/FeatureSource"; | ||||||
| import {TagsFilter} from "../Tags/TagsFilter"; | import {TagsFilter} from "../Tags/TagsFilter"; | ||||||
| import {Tag} from "../Tags/Tag"; | import {Tag} from "../Tags/Tag"; | ||||||
| import {OsmConnection} from "./OsmConnection"; |  | ||||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Handles all changes made to OSM. |  * Handles all changes made to OSM. | ||||||
|  | @ -16,23 +14,18 @@ import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||||
| export class Changes implements FeatureSource{ | export class Changes implements FeatureSource{ | ||||||
| 
 | 
 | ||||||
|      |      | ||||||
|     private static _nextId = -1; // Newly assigned ID's are negative
 |  | ||||||
|     public readonly name = "Newly added features" |     public readonly name = "Newly added features" | ||||||
|     /** |     /** | ||||||
|      * The newly created points, as a FeatureSource |      * The newly created points, as a FeatureSource | ||||||
|      */ |      */ | ||||||
|     public features = new UIEventSource<{feature: any, freshness: Date}[]>([]); |     public features = new UIEventSource<{feature: any, freshness: Date}[]>([]); | ||||||
|  |      | ||||||
|  |     private static _nextId = -1; // Newly assigned ID's are negative
 | ||||||
|     /** |     /** | ||||||
|      * All the pending changes |      * All the pending changes | ||||||
|      */ |      */ | ||||||
|     public readonly pending = LocalStorageSource.GetParsed<{ elementId: string, key: string, value: string }[]>("pending-changes", []) |     public readonly pending: UIEventSource<{ elementId: string, key: string, value: string }[]> =  | ||||||
| 
 |         new UIEventSource<{elementId: string; key: string; value: string}[]>([]); | ||||||
|     /** |  | ||||||
|      * All the pending new objects to upload |  | ||||||
|      */ |  | ||||||
|     private readonly newObjects = LocalStorageSource.GetParsed<{ id: number, lat: number, lon: number }[]>("newObjects", []) |  | ||||||
| 
 |  | ||||||
|     private readonly isUploading = new UIEventSource(false); |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Adds a change to the pending changes |      * Adds a change to the pending changes | ||||||
|  | @ -57,6 +50,7 @@ export class Changes implements FeatureSource { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|      |      | ||||||
|  |      | ||||||
|     addTag(elementId: string, tagsFilter: TagsFilter, |     addTag(elementId: string, tagsFilter: TagsFilter, | ||||||
|            tags?: UIEventSource<any>) { |            tags?: UIEventSource<any>) { | ||||||
|         const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); |         const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); | ||||||
|  | @ -89,9 +83,9 @@ export class Changes implements FeatureSource { | ||||||
|         if(flushreason !== undefined){ |         if(flushreason !== undefined){ | ||||||
|             console.log(flushreason) |             console.log(flushreason) | ||||||
|         } |         } | ||||||
|         this.uploadAll(); |         this.uploadAll([], this.pending.data); | ||||||
|  |         this.pending.setData([]); | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Create a new node element at the given lat/long. |      * Create a new node element at the given lat/long. | ||||||
|      * An internal OsmObject is created to upload later on, a geojson represention is returned. |      * An internal OsmObject is created to upload later on, a geojson represention is returned. | ||||||
|  | @ -99,12 +93,12 @@ export class Changes implements FeatureSource { | ||||||
|      */ |      */ | ||||||
|     public createElement(basicTags: Tag[], lat: number, lon: number) { |     public createElement(basicTags: Tag[], lat: number, lon: number) { | ||||||
|         console.log("Creating a new element with ", basicTags) |         console.log("Creating a new element with ", basicTags) | ||||||
|         const newId = Changes._nextId; |         const osmNode = new OsmNode(Changes._nextId); | ||||||
|         Changes._nextId--; |         Changes._nextId--; | ||||||
| 
 | 
 | ||||||
|         const id = "node/" + newId; |         const id = "node/" + osmNode.id; | ||||||
| 
 |         osmNode.lat = lat; | ||||||
| 
 |         osmNode.lon = lon; | ||||||
|         const properties = {id: id}; |         const properties = {id: id}; | ||||||
| 
 | 
 | ||||||
|         const geojson = { |         const geojson = { | ||||||
|  | @ -124,10 +118,10 @@ export class Changes implements FeatureSource { | ||||||
|         // The tags are not yet written into the OsmObject, but this is applied onto a 
 |         // The tags are not yet written into the OsmObject, but this is applied onto a 
 | ||||||
|         const changes = []; |         const changes = []; | ||||||
|         for (const kv of basicTags) { |         for (const kv of basicTags) { | ||||||
|  |             properties[kv.key] = kv.value; | ||||||
|             if (typeof kv.value !== "string") { |             if (typeof kv.value !== "string") { | ||||||
|                 throw "Invalid value: don't use a regex in a preset" |                 throw "Invalid value: don't use a regex in a preset" | ||||||
|             } |             } | ||||||
|             properties[kv.key] = kv.value; |  | ||||||
|             changes.push({elementId: id, key: kv.key, value: kv.value}) |             changes.push({elementId: id, key: kv.key, value: kv.value}) | ||||||
|         } |         } | ||||||
|         |         | ||||||
|  | @ -137,36 +131,22 @@ export class Changes implements FeatureSource { | ||||||
|          |          | ||||||
|         State.state.allElements.addOrGetElement(geojson).ping(); |         State.state.allElements.addOrGetElement(geojson).ping(); | ||||||
| 
 | 
 | ||||||
|         if (State.state.osmConnection.userDetails.data.backend !== OsmConnection.oauth_configs.osm.url) { |         this.uploadAll([osmNode], changes); | ||||||
|             properties["_backend"] = State.state.osmConnection.userDetails.data.backend |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         this.newObjects.data.push({id: newId, lat: lat, lon: lon}) |  | ||||||
|         this.pending.data.push(...changes) |  | ||||||
|         this.pending.ping(); |  | ||||||
|         this.newObjects.ping(); |  | ||||||
|         return geojson; |         return geojson; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private uploadChangesWithLatestVersions( |     private uploadChangesWithLatestVersions( | ||||||
|         knownElements: OsmObject[]) { |         knownElements: OsmObject[], newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) { | ||||||
|         const knownById = new Map<string, OsmObject>(); |         const knownById = new Map<string, OsmObject>(); | ||||||
|  |          | ||||||
|         knownElements.forEach(knownElement => { |         knownElements.forEach(knownElement => { | ||||||
|             knownById.set(knownElement.type + "/" + knownElement.id, knownElement) |             knownById.set(knownElement.type + "/" + knownElement.id, knownElement) | ||||||
|         }) |         }) | ||||||
|          |          | ||||||
|         const newElements: OsmNode [] = this.newObjects.data.map(spec => { |  | ||||||
|             const newElement = new OsmNode(spec.id); |  | ||||||
|             newElement.lat = spec.lat; |  | ||||||
|             newElement.lon = spec.lon; |  | ||||||
|             return newElement |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|           |           | ||||||
|         // Here, inside the continuation, we know that all 'neededIds' are loaded in 'knownElements', which maps the ids onto the elements
 |         // Here, inside the continuation, we know that all 'neededIds' are loaded in 'knownElements', which maps the ids onto the elements
 | ||||||
|         // We apply the changes on them
 |         // We apply the changes on them
 | ||||||
|         for (const change of this.pending.data) { |         for (const change of pending) { | ||||||
|             if (parseInt(change.elementId.split("/")[1]) < 0) { |             if (parseInt(change.elementId.split("/")[1]) < 0) { | ||||||
|                 // This is a new element - we should apply this on one of the new elements
 |                 // This is a new element - we should apply this on one of the new elements
 | ||||||
|                 for (const newElement of newElements) { |                 for (const newElement of newElements) { | ||||||
|  | @ -188,17 +168,9 @@ export class Changes implements FeatureSource { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if (changedElements.length == 0 && newElements.length == 0) { |         if (changedElements.length == 0 && newElements.length == 0) { | ||||||
|             console.log("No changes in any object - clearing"); |             console.log("No changes in any object"); | ||||||
|             this.pending.setData([]) |  | ||||||
|             this.newObjects.setData([]) |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         const self = this; |  | ||||||
| 
 |  | ||||||
|         if (this.isUploading.data) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         this.isUploading.setData(true) |  | ||||||
| 
 | 
 | ||||||
|         console.log("Beginning upload..."); |         console.log("Beginning upload..."); | ||||||
|         // At last, we build the changeset and upload
 |         // At last, we build the changeset and upload
 | ||||||
|  | @ -241,22 +213,17 @@ export class Changes implements FeatureSource { | ||||||
|                 changes += "</osmChange>"; |                 changes += "</osmChange>"; | ||||||
| 
 | 
 | ||||||
|                 return changes; |                 return changes; | ||||||
|             }, |             }); | ||||||
|             () => { |  | ||||||
|                 console.log("Upload successfull!") |  | ||||||
|                 self.newObjects.setData([]) |  | ||||||
|                 self.pending.setData([]); |  | ||||||
|                 self.isUploading.setData(false) |  | ||||||
|             }, |  | ||||||
|             () => self.isUploading.setData(false) |  | ||||||
|         ); |  | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     private uploadAll() { |     private uploadAll( | ||||||
|  |         newElements: OsmObject[], | ||||||
|  |         pending: { elementId: string; key: string; value: string }[] | ||||||
|  |     ) { | ||||||
|         const self = this; |         const self = this; | ||||||
| 
 | 
 | ||||||
|         const pending = this.pending.data; | 
 | ||||||
|         let neededIds: string[] = []; |         let neededIds: string[] = []; | ||||||
|         for (const change of pending) { |         for (const change of pending) { | ||||||
|             const id = change.elementId; |             const id = change.elementId; | ||||||
|  | @ -269,7 +236,8 @@ export class Changes implements FeatureSource { | ||||||
| 
 | 
 | ||||||
|         neededIds = Utils.Dedup(neededIds); |         neededIds = Utils.Dedup(neededIds); | ||||||
|         OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => { |         OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => { | ||||||
|             self.uploadChangesWithLatestVersions(knownElements) |             console.log("KnownElements:", knownElements) | ||||||
|  |             self.uploadChangesWithLatestVersions(knownElements, newElements, pending) | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ export class ChangesetHandler { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage): void { |     private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) { | ||||||
|         const nodes = response.getElementsByTagName("node"); |         const nodes = response.getElementsByTagName("node"); | ||||||
|         // @ts-ignore
 |         // @ts-ignore
 | ||||||
|         for (const node of nodes) { |         for (const node of nodes) { | ||||||
|  | @ -69,9 +69,7 @@ export class ChangesetHandler { | ||||||
|     public UploadChangeset( |     public UploadChangeset( | ||||||
|         layout: LayoutConfig, |         layout: LayoutConfig, | ||||||
|         allElements: ElementStorage, |         allElements: ElementStorage, | ||||||
|         generateChangeXML: (csid: string) => string, |         generateChangeXML: (csid: string) => string) { | ||||||
|         whenDone: (csId: string) => void, |  | ||||||
|         onFail: () => void) { |  | ||||||
| 
 | 
 | ||||||
|         if (this.userDetails.data.csCount == 0) { |         if (this.userDetails.data.csCount == 0) { | ||||||
|             // The user became a contributor!
 |             // The user became a contributor!
 | ||||||
|  | @ -82,7 +80,6 @@ export class ChangesetHandler { | ||||||
|         if (this._dryRun) { |         if (this._dryRun) { | ||||||
|             const changesetXML = generateChangeXML("123456"); |             const changesetXML = generateChangeXML("123456"); | ||||||
|             console.log(changesetXML); |             console.log(changesetXML); | ||||||
|             whenDone("123456") |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -96,14 +93,12 @@ export class ChangesetHandler { | ||||||
|                 console.log(changeset); |                 console.log(changeset); | ||||||
|                 self.AddChange(csId, changeset, |                 self.AddChange(csId, changeset, | ||||||
|                     allElements, |                     allElements, | ||||||
|                     whenDone, |                     () => { | ||||||
|  |                     }, | ||||||
|                     (e) => { |                     (e) => { | ||||||
|                         console.error("UPLOADING FAILED!", e) |                         console.error("UPLOADING FAILED!", e) | ||||||
|                         onFail() |  | ||||||
|                     } |                     } | ||||||
|                 ) |                 ) | ||||||
|             }, { |  | ||||||
|                 onFail: onFail |  | ||||||
|             }) |             }) | ||||||
|         } else { |         } else { | ||||||
|             // There still exists an open changeset (or at least we hope so)
 |             // There still exists an open changeset (or at least we hope so)
 | ||||||
|  | @ -112,13 +107,15 @@ export class ChangesetHandler { | ||||||
|                 csId, |                 csId, | ||||||
|                 generateChangeXML(csId), |                 generateChangeXML(csId), | ||||||
|                 allElements, |                 allElements, | ||||||
|                 whenDone, |                 () => { | ||||||
|  |                 }, | ||||||
|                 (e) => { |                 (e) => { | ||||||
|                     console.warn("Could not upload, changeset is probably closed: ", e); |                     console.warn("Could not upload, changeset is probably closed: ", e); | ||||||
|                     // Mark the CS as closed...
 |                     // Mark the CS as closed...
 | ||||||
|                     this.currentChangeset.setData(""); |                     this.currentChangeset.setData(""); | ||||||
|                     // ... and try again. As the cs is closed, no recursive loop can exist  
 |                     // ... and try again. As the cs is closed, no recursive loop can exist  
 | ||||||
|                     self.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail); |                     self.UploadChangeset(layout, allElements, generateChangeXML); | ||||||
|  | 
 | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  | @ -175,11 +172,7 @@ export class ChangesetHandler { | ||||||
|                 // FAILED
 |                 // FAILED
 | ||||||
|                 self.CloseChangeset(csId, continuation) |                 self.CloseChangeset(csId, continuation) | ||||||
|             }) |             }) | ||||||
|             }, { |         }, true, reason) | ||||||
|                 isDeletionCS: true, |  | ||||||
|                 deletionReason: reason |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => { |     private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => { | ||||||
|  | @ -211,20 +204,15 @@ export class ChangesetHandler { | ||||||
|     private OpenChangeset( |     private OpenChangeset( | ||||||
|         layout: LayoutConfig, |         layout: LayoutConfig, | ||||||
|         continuation: (changesetId: string) => void, |         continuation: (changesetId: string) => void, | ||||||
|         options?: { |         isDeletionCS: boolean = false, | ||||||
|             isDeletionCS?: boolean, |         deletionReason: string = undefined) { | ||||||
|             deletionReason?: string, | 
 | ||||||
|             onFail?: () => void |  | ||||||
|         } |  | ||||||
|     ) { |  | ||||||
|         options = options ?? {} |  | ||||||
|         options.isDeletionCS = options.isDeletionCS ?? false |  | ||||||
|         const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : ""; |         const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : ""; | ||||||
|         let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}` |         let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}` | ||||||
|         if (options.isDeletionCS) { |         if (isDeletionCS) { | ||||||
|             comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}` |             comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}` | ||||||
|             if (options.deletionReason) { |             if (deletionReason) { | ||||||
|                 comment += ": " + options.deletionReason; |                 comment += ": " + deletionReason; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -233,7 +221,7 @@ export class ChangesetHandler { | ||||||
|         const metadata = [ |         const metadata = [ | ||||||
|             ["created_by", `MapComplete ${Constants.vNumber}`], |             ["created_by", `MapComplete ${Constants.vNumber}`], | ||||||
|             ["comment", comment], |             ["comment", comment], | ||||||
|             ["deletion", options.isDeletionCS ? "yes" : undefined], |             ["deletion", isDeletionCS ? "yes" : undefined], | ||||||
|             ["theme", layout.id], |             ["theme", layout.id], | ||||||
|             ["language", Locale.language.data], |             ["language", Locale.language.data], | ||||||
|             ["host", window.location.host], |             ["host", window.location.host], | ||||||
|  | @ -256,9 +244,7 @@ export class ChangesetHandler { | ||||||
|         }, function (err, response) { |         }, function (err, response) { | ||||||
|             if (response === undefined) { |             if (response === undefined) { | ||||||
|                 console.log("err", err); |                 console.log("err", err); | ||||||
|                 if(options.onFail){ |                 alert("Could not upload change (opening failed). Please file a bug report") | ||||||
|                     options.onFail() |  | ||||||
|                 } |  | ||||||
|                 return; |                 return; | ||||||
|             } else { |             } else { | ||||||
|                 continuation(response); |                 continuation(response); | ||||||
|  | @ -279,7 +265,7 @@ export class ChangesetHandler { | ||||||
|     private AddChange(changesetId: string, |     private AddChange(changesetId: string, | ||||||
|                       changesetXML: string, |                       changesetXML: string, | ||||||
|                       allElements: ElementStorage, |                       allElements: ElementStorage, | ||||||
|                       continuation: ((changesetId: string) => void), |                       continuation: ((changesetId: string, idMapping: any) => void), | ||||||
|                       onFail: ((changesetId: string, reason: string) => void) = undefined) { |                       onFail: ((changesetId: string, reason: string) => void) = undefined) { | ||||||
|         this.auth.xhr({ |         this.auth.xhr({ | ||||||
|             method: 'POST', |             method: 'POST', | ||||||
|  | @ -294,9 +280,9 @@ export class ChangesetHandler { | ||||||
|                 } |                 } | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             ChangesetHandler.parseUploadChangesetResponse(response, allElements); |             const mapping = ChangesetHandler.parseUploadChangesetResponse(response, allElements); | ||||||
|             console.log("Uploaded changeset ", changesetId); |             console.log("Uploaded changeset ", changesetId); | ||||||
|             continuation(changesetId); |             continuation(changesetId, mapping); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -30,7 +30,7 @@ export default class UserDetails { | ||||||
| 
 | 
 | ||||||
| export class OsmConnection { | export class OsmConnection { | ||||||
| 
 | 
 | ||||||
|     public static readonly oauth_configs = { |     public static readonly _oauth_configs = { | ||||||
|         "osm": { |         "osm": { | ||||||
|             oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem', |             oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem', | ||||||
|             oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI', |             oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI', | ||||||
|  | @ -47,7 +47,6 @@ export class OsmConnection { | ||||||
|     public auth; |     public auth; | ||||||
|     public userDetails: UIEventSource<UserDetails>; |     public userDetails: UIEventSource<UserDetails>; | ||||||
|     public isLoggedIn: UIEventSource<boolean> |     public isLoggedIn: UIEventSource<boolean> | ||||||
|     private fakeUser: boolean; |  | ||||||
|     _dryRun: boolean; |     _dryRun: boolean; | ||||||
|     public preferencesHandler: OsmPreferences; |     public preferencesHandler: OsmPreferences; | ||||||
|     public changesetHandler: ChangesetHandler; |     public changesetHandler: ChangesetHandler; | ||||||
|  | @ -60,31 +59,20 @@ export class OsmConnection { | ||||||
|         url: string |         url: string | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     constructor(dryRun: boolean,  |     constructor(dryRun: boolean, oauth_token: UIEventSource<string>, | ||||||
|                 fakeUser: boolean, |  | ||||||
|                 oauth_token: UIEventSource<string>, |  | ||||||
|                 // Used to keep multiple changesets open and to write to the correct changeset
 |                 // Used to keep multiple changesets open and to write to the correct changeset
 | ||||||
|                 layoutName: string, |                 layoutName: string, | ||||||
|                 singlePage: boolean = true, |                 singlePage: boolean = true, | ||||||
|                 osmConfiguration: "osm" | "osm-test" = 'osm' |                 osmConfiguration: "osm" | "osm-test" = 'osm' | ||||||
|     ) { |     ) { | ||||||
|         this.fakeUser = fakeUser; |  | ||||||
|         this._singlePage = singlePage; |         this._singlePage = singlePage; | ||||||
|         this._oauth_config = OsmConnection.oauth_configs[osmConfiguration] ?? OsmConnection.oauth_configs.osm; |         this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm; | ||||||
|         console.debug("Using backend", this._oauth_config.url) |         console.debug("Using backend", this._oauth_config.url) | ||||||
|         OsmObject.SetBackendUrl(this._oauth_config.url + "/") |         OsmObject.SetBackendUrl(this._oauth_config.url + "/") | ||||||
|         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 = dryRun || fakeUser; |         this.userDetails.data.dryRun = dryRun; | ||||||
|         if(fakeUser){ |  | ||||||
|             const ud = this.userDetails.data; |  | ||||||
|             ud.csCount = 5678 |  | ||||||
|             ud.loggedIn= true; |  | ||||||
|             ud.unreadMessages = 0 |  | ||||||
|             ud.name = "Fake user" |  | ||||||
|             ud.totalMessages = 42; |  | ||||||
|         } |  | ||||||
|         const self =this; |         const self =this; | ||||||
|         this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => { |         this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => { | ||||||
|             if(self.userDetails.data.loggedIn == false && isLoggedIn == true){ |             if(self.userDetails.data.loggedIn == false && isLoggedIn == true){ | ||||||
|  | @ -122,10 +110,8 @@ export class OsmConnection { | ||||||
|     public UploadChangeset( |     public UploadChangeset( | ||||||
|         layout: LayoutConfig, |         layout: LayoutConfig, | ||||||
|         allElements: ElementStorage, |         allElements: ElementStorage, | ||||||
|         generateChangeXML: (csid: string) => string, |         generateChangeXML: (csid: string) => string) { | ||||||
|         whenDone: (csId: string) => void, |         this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML); | ||||||
|         onFail: () => {}) { |  | ||||||
|         this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { |     public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { | ||||||
|  | @ -150,10 +136,6 @@ export class OsmConnection { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public AttemptLogin() { |     public AttemptLogin() { | ||||||
|         if(this.fakeUser){ |  | ||||||
|             console.log("AttemptLogin called, but ignored as fakeUser is set") |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         const self = this; |         const self = this; | ||||||
|         console.log("Trying to log in..."); |         console.log("Trying to log in..."); | ||||||
|         this.updateAuthObject(); |         this.updateAuthObject(); | ||||||
|  |  | ||||||
|  | @ -5,8 +5,7 @@ import {UIEventSource} from "../UIEventSource"; | ||||||
| 
 | 
 | ||||||
| export abstract class OsmObject { | export abstract class OsmObject { | ||||||
| 
 | 
 | ||||||
|     private static defaultBackend = "https://www.openstreetmap.org/" |     protected static backendURL = "https://www.openstreetmap.org/" | ||||||
|     protected static backendURL = OsmObject.defaultBackend; |  | ||||||
|     private static polygonFeatures = OsmObject.constructPolygonFeatures() |     private static polygonFeatures = OsmObject.constructPolygonFeatures() | ||||||
|     private static objectCache = new Map<string, UIEventSource<OsmObject>>(); |     private static objectCache = new Map<string, UIEventSource<OsmObject>>(); | ||||||
|     private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>(); |     private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>(); | ||||||
|  | @ -292,7 +291,6 @@ export abstract class OsmObject { | ||||||
| 
 | 
 | ||||||
|                 self.LoadData(element) |                 self.LoadData(element) | ||||||
|                 self.SaveExtraData(element, nodes); |                 self.SaveExtraData(element, nodes); | ||||||
| 
 |  | ||||||
|                 const meta = { |                 const meta = { | ||||||
|                     "_last_edit:contributor": element.user, |                     "_last_edit:contributor": element.user, | ||||||
|                     "_last_edit:contributor:uid": element.uid, |                     "_last_edit:contributor:uid": element.uid, | ||||||
|  | @ -301,11 +299,6 @@ export abstract class OsmObject { | ||||||
|                     "_version_number": element.version |                     "_version_number": element.version | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (OsmObject.backendURL !== OsmObject.defaultBackend) { |  | ||||||
|                     self.tags["_backend"] = OsmObject.backendURL |  | ||||||
|                     meta["_backend"] = OsmObject.backendURL; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 continuation(self, meta); |                 continuation(self, meta); | ||||||
|             } |             } | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|  | @ -83,8 +83,7 @@ export default class SimpleMetaTagger { | ||||||
| 
 | 
 | ||||||
|         }, |         }, | ||||||
|         (feature => { |         (feature => { | ||||||
|             const units = State.state?.layoutToUse?.data?.units ?? []; |             const units = State.state.layoutToUse.data.units ?? []; | ||||||
|             let rewritten = false; |  | ||||||
|             for (const key in feature.properties) { |             for (const key in feature.properties) { | ||||||
|                 if (!feature.properties.hasOwnProperty(key)) { |                 if (!feature.properties.hasOwnProperty(key)) { | ||||||
|                     continue; |                     continue; | ||||||
|  | @ -96,23 +95,16 @@ export default class SimpleMetaTagger { | ||||||
|                     const value = feature.properties[key] |                     const value = feature.properties[key] | ||||||
|                     const [, denomination] = unit.findDenomination(value) |                     const [, denomination] = unit.findDenomination(value) | ||||||
|                     let canonical = denomination?.canonicalValue(value) ?? undefined; |                     let canonical = denomination?.canonicalValue(value) ?? undefined; | ||||||
|                     if(canonical === value){ |                     console.log("Rewritten ", key, " from", value, "into", canonical) | ||||||
|                         break; |  | ||||||
|                     } |  | ||||||
|                     console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`) |  | ||||||
|                     if(canonical === undefined && !unit.eraseInvalid) { |                     if(canonical === undefined && !unit.eraseInvalid) { | ||||||
|                         break; |                         break; | ||||||
|                     } |                     } | ||||||
|                      |                      | ||||||
|                     feature.properties[key] = canonical; |                     feature.properties[key] = canonical; | ||||||
|                     rewritten = true; |  | ||||||
|                     break; |                     break; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|             } |             } | ||||||
|             if(rewritten){ |  | ||||||
|                 State.state.allElements.getEventSourceById(feature.id).ping(); |  | ||||||
|             } |  | ||||||
|         }) |         }) | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,22 +5,6 @@ import {UIEventSource} from "../UIEventSource"; | ||||||
|  */ |  */ | ||||||
| export class LocalStorageSource { | export class LocalStorageSource { | ||||||
| 
 | 
 | ||||||
|     static GetParsed<T>(key: string, defaultValue : T) : UIEventSource<T>{ |  | ||||||
|         return LocalStorageSource.Get(key).map( |  | ||||||
|             str => { |  | ||||||
|                 if(str === undefined){ |  | ||||||
|                     return defaultValue |  | ||||||
|                 } |  | ||||||
|                 try{ |  | ||||||
|                     return JSON.parse(str) |  | ||||||
|                 }catch{ |  | ||||||
|                     return defaultValue |  | ||||||
|                 } |  | ||||||
|             }, [],  |  | ||||||
|             value => JSON.stringify(value) |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static Get(key: string, defaultValue: string = undefined): UIEventSource<string> { |     static Get(key: string, defaultValue: string = undefined): UIEventSource<string> { | ||||||
|         try { |         try { | ||||||
|             const saved = localStorage.getItem(key); |             const saved = localStorage.getItem(key); | ||||||
|  |  | ||||||
|  | @ -7,6 +7,4 @@ export default interface BaseLayer { | ||||||
|     max_zoom: number, |     max_zoom: number, | ||||||
|     min_zoom: number; |     min_zoom: number; | ||||||
|     feature: any, |     feature: any, | ||||||
|     isBest?: boolean, |  | ||||||
|     category?: "map" | "osmbasedmap" | "photo"  | "historicphoto" | string |  | ||||||
| } | } | ||||||
|  | @ -2,7 +2,7 @@ import { Utils } from "../Utils"; | ||||||
| 
 | 
 | ||||||
| export default class Constants { | export default class Constants { | ||||||
|      |      | ||||||
|     public static vNumber = "0.8.4-rc3"; |     public static vNumber = "0.8.3f"; | ||||||
| 
 | 
 | ||||||
|     // The user journey states thresholds when a new feature gets unlocked
 |     // The user journey states thresholds when a new feature gets unlocked
 | ||||||
|     public static userJourney = { |     public static userJourney = { | ||||||
|  |  | ||||||
|  | @ -1,8 +0,0 @@ | ||||||
| export interface TileRange { |  | ||||||
|     xstart: number, |  | ||||||
|     ystart: number, |  | ||||||
|     xend: number, |  | ||||||
|     yend: number, |  | ||||||
|     total: number, |  | ||||||
|     zoomlevel: number |  | ||||||
| } |  | ||||||
							
								
								
									
										39
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										39
									
								
								State.ts
									
										
									
									
									
								
							|  | @ -19,7 +19,6 @@ import TitleHandler from "./Logic/Actors/TitleHandler"; | ||||||
| import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; | import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; | ||||||
| import { Relation } from "./Logic/Osm/ExtractRelations"; | import { Relation } from "./Logic/Osm/ExtractRelations"; | ||||||
| import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; | import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; | ||||||
| import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Contains the global state: a bunch of UI-event sources |  * Contains the global state: a bunch of UI-event sources | ||||||
|  | @ -60,13 +59,17 @@ export default class State { | ||||||
| 
 | 
 | ||||||
|   public osmApiFeatureSource: OsmApiFeatureSource; |   public osmApiFeatureSource: OsmApiFeatureSource; | ||||||
| 
 | 
 | ||||||
|     public filteredLayers: UIEventSource<{ |   public filteredLayers: UIEventSource< | ||||||
|  |     { | ||||||
|       readonly isDisplayed: UIEventSource<boolean>; |       readonly isDisplayed: UIEventSource<boolean>; | ||||||
|       readonly layerDef: LayerConfig; |       readonly layerDef: LayerConfig; | ||||||
|     }[]> = new UIEventSource<{ |     }[] | ||||||
|  |   > = new UIEventSource< | ||||||
|  |     { | ||||||
|       readonly isDisplayed: UIEventSource<boolean>; |       readonly isDisplayed: UIEventSource<boolean>; | ||||||
|       readonly layerDef: LayerConfig; |       readonly layerDef: LayerConfig; | ||||||
|     }[]>([]); |     }[] | ||||||
|  |   >([]); | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|      The latest element that was selected |      The latest element that was selected | ||||||
|  | @ -80,7 +83,9 @@ export default class State { | ||||||
|    * Keeps track of relations: which way is part of which other way? |    * Keeps track of relations: which way is part of which other way? | ||||||
|    * Set by the overpass-updater; used in the metatagging |    * Set by the overpass-updater; used in the metatagging | ||||||
|    */ |    */ | ||||||
|     public readonly knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(undefined, "Relation memberships"); |   public readonly knownRelations = new UIEventSource< | ||||||
|  |     Map<string, { role: string; relation: Relation }[]> | ||||||
|  |   >(undefined, "Relation memberships"); | ||||||
| 
 | 
 | ||||||
|   public readonly featureSwitchUserbadge: UIEventSource<boolean>; |   public readonly featureSwitchUserbadge: UIEventSource<boolean>; | ||||||
|   public readonly featureSwitchSearch: UIEventSource<boolean>; |   public readonly featureSwitchSearch: UIEventSource<boolean>; | ||||||
|  | @ -96,12 +101,6 @@ export default class State { | ||||||
|   public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>; |   public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>; | ||||||
|   public readonly featureSwitchApiURL: UIEventSource<string>; |   public readonly featureSwitchApiURL: UIEventSource<string>; | ||||||
|   public readonly featureSwitchFilter: UIEventSource<boolean>; |   public readonly featureSwitchFilter: UIEventSource<boolean>; | ||||||
|     public readonly featureSwitchEnableExport: UIEventSource<boolean>; |  | ||||||
|     public readonly featureSwitchFakeUser: UIEventSource<boolean>; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     public featurePipeline: FeaturePipeline; |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * The map location: currently centered lat, lon and zoom |    * The map location: currently centered lat, lon and zoom | ||||||
|  | @ -128,7 +127,9 @@ export default class State { | ||||||
|     accuracy: number; |     accuracy: number; | ||||||
|   }>(undefined); |   }>(undefined); | ||||||
|   public layoutDefinition: string; |   public layoutDefinition: string; | ||||||
|     public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; |   public installedThemes: UIEventSource< | ||||||
|  |     { layout: LayoutConfig; definition: string }[] | ||||||
|  |   >; | ||||||
| 
 | 
 | ||||||
|   public layerControlIsOpened: UIEventSource<boolean> = |   public layerControlIsOpened: UIEventSource<boolean> = | ||||||
|     QueryParameters.GetQueryParameter( |     QueryParameters.GetQueryParameter( | ||||||
|  | @ -318,24 +319,11 @@ export default class State { | ||||||
|         (b) => "" + b |         (b) => "" + b | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|             this.featureSwitchFakeUser = QueryParameters.GetQueryParameter("fake-user", "false", |  | ||||||
|                 "If true, 'dryrun' mode is activated and a fake user account is loaded") |  | ||||||
|                 .map(str => str === "true", [], b => "" + b); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|       this.featureSwitchApiURL = QueryParameters.GetQueryParameter( |       this.featureSwitchApiURL = QueryParameters.GetQueryParameter( | ||||||
|         "backend", |         "backend", | ||||||
|         "osm", |         "osm", | ||||||
|         "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'" |         "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'" | ||||||
|       ); |       ); | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             this.featureSwitchUserbadge.addCallbackAndRun(userbadge => { |  | ||||||
|                 if (!userbadge) { |  | ||||||
|                     this.featureSwitchAddNew.setData(false) |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|     } |     } | ||||||
|     { |     { | ||||||
|       // Some other feature switches
 |       // Some other feature switches
 | ||||||
|  | @ -361,7 +349,6 @@ export default class State { | ||||||
| 
 | 
 | ||||||
|     this.osmConnection = new OsmConnection( |     this.osmConnection = new OsmConnection( | ||||||
|       this.featureSwitchIsTesting.data, |       this.featureSwitchIsTesting.data, | ||||||
|             this.featureSwitchFakeUser.data, |  | ||||||
|       QueryParameters.GetQueryParameter( |       QueryParameters.GetQueryParameter( | ||||||
|         "oauth_token", |         "oauth_token", | ||||||
|         undefined, |         undefined, | ||||||
|  |  | ||||||
							
								
								
									
										32
									
								
								Svg.ts
									
										
									
									
									
								
							
							
						
						
									
										32
									
								
								Svg.ts
									
										
									
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -5,7 +5,6 @@ import Loc from "../../Models/Loc"; | ||||||
| import BaseLayer from "../../Models/BaseLayer"; | import BaseLayer from "../../Models/BaseLayer"; | ||||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||||
| import {Map} from "leaflet"; | import {Map} from "leaflet"; | ||||||
| import {Utils} from "../../Utils"; |  | ||||||
| 
 | 
 | ||||||
| export default class Minimap extends BaseUIElement { | export default class Minimap extends BaseUIElement { | ||||||
| 
 | 
 | ||||||
|  | @ -16,13 +15,11 @@ export default class Minimap extends BaseUIElement { | ||||||
|     private readonly _location: UIEventSource<Loc>; |     private readonly _location: UIEventSource<Loc>; | ||||||
|     private _isInited = false; |     private _isInited = false; | ||||||
|     private _allowMoving: boolean; |     private _allowMoving: boolean; | ||||||
|     private readonly _leafletoptions: any; |  | ||||||
| 
 | 
 | ||||||
|     constructor(options?: { |     constructor(options?: { | ||||||
|                     background?: UIEventSource<BaseLayer>, |                     background?: UIEventSource<BaseLayer>, | ||||||
|                     location?: UIEventSource<Loc>, |                     location?: UIEventSource<Loc>, | ||||||
|                     allowMoving?: boolean, |                     allowMoving?: boolean | ||||||
|                     leafletOptions?: any |  | ||||||
|                 } |                 } | ||||||
|     ) { |     ) { | ||||||
|         super() |         super() | ||||||
|  | @ -31,7 +28,6 @@ export default class Minimap extends BaseUIElement { | ||||||
|         this._location = options?.location ?? new UIEventSource<Loc>(undefined) |         this._location = options?.location ?? new UIEventSource<Loc>(undefined) | ||||||
|         this._id = "minimap" + Minimap._nextId; |         this._id = "minimap" + Minimap._nextId; | ||||||
|         this._allowMoving = options.allowMoving ?? true; |         this._allowMoving = options.allowMoving ?? true; | ||||||
|         this._leafletoptions = options.leafletOptions ?? {} |  | ||||||
|         Minimap._nextId++ |         Minimap._nextId++ | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  | @ -48,6 +44,7 @@ export default class Minimap extends BaseUIElement { | ||||||
|         const self = this; |         const self = this; | ||||||
|         // @ts-ignore
 |         // @ts-ignore
 | ||||||
|         const resizeObserver = new ResizeObserver(_ => { |         const resizeObserver = new ResizeObserver(_ => { | ||||||
|  |             console.log("Change in size detected!") | ||||||
|             self.InitMap(); |             self.InitMap(); | ||||||
|             self.leafletMap?.data?.invalidateSize() |             self.leafletMap?.data?.invalidateSize() | ||||||
|         }); |         }); | ||||||
|  | @ -75,8 +72,8 @@ export default class Minimap extends BaseUIElement { | ||||||
|         const location = this._location; |         const location = this._location; | ||||||
| 
 | 
 | ||||||
|         let currentLayer = this._background.data.layer() |         let currentLayer = this._background.data.layer() | ||||||
|         const options = { |         const map = L.map(this._id, { | ||||||
|             center: <[number, number]> [location.data?.lat ?? 0, location.data?.lon ?? 0], |             center: [location.data?.lat ?? 0, location.data?.lon ?? 0], | ||||||
|             zoom: location.data?.zoom ?? 2, |             zoom: location.data?.zoom ?? 2, | ||||||
|             layers: [currentLayer], |             layers: [currentLayer], | ||||||
|             zoomControl: false, |             zoomControl: false, | ||||||
|  | @ -85,14 +82,8 @@ export default class Minimap extends BaseUIElement { | ||||||
|             scrollWheelZoom: this._allowMoving, |             scrollWheelZoom: this._allowMoving, | ||||||
|             doubleClickZoom: this._allowMoving, |             doubleClickZoom: this._allowMoving, | ||||||
|             keyboard: this._allowMoving, |             keyboard: this._allowMoving, | ||||||
|             touchZoom: this._allowMoving, |             touchZoom: this._allowMoving | ||||||
|             // Disabling this breaks the geojson layer - don't ask me why!  zoomAnimation: this._allowMoving,
 |         }); | ||||||
|             fadeAnimation: this._allowMoving |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         Utils.Merge(this._leafletoptions, options) |  | ||||||
|          |  | ||||||
|         const map = L.map(this._id, options); |  | ||||||
| 
 | 
 | ||||||
|         map.setMaxBounds( |         map.setMaxBounds( | ||||||
|             [[-100, -200], [100, 200]] |             [[-100, -200], [100, 200]] | ||||||
|  |  | ||||||
|  | @ -3,7 +3,6 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc"; | ||||||
| import BaseLayer from "../../Models/BaseLayer"; | import BaseLayer from "../../Models/BaseLayer"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; |  | ||||||
| 
 | 
 | ||||||
| export class Basemap { | export class Basemap { | ||||||
| 
 | 
 | ||||||
|  | @ -36,8 +35,9 @@ export class Basemap { | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         this.map.attributionControl.setPrefix( |         this.map.attributionControl.setPrefix( | ||||||
|             "<span id='leaflet-attribution'>A</span>"); |             "<span id='leaflet-attribution'></span> | <a href='https://osm.org'>OpenStreetMap</a>"); | ||||||
| 
 | 
 | ||||||
|  |         extraAttribution.AttachTo('leaflet-attribution') | ||||||
|         const self = this; |         const self = this; | ||||||
| 
 | 
 | ||||||
|         currentLayer.addCallbackAndRun(layer => { |         currentLayer.addCallbackAndRun(layer => { | ||||||
|  | @ -77,7 +77,6 @@ export class Basemap { | ||||||
|             lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng}); |             lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng}); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         extraAttribution.AttachTo('leaflet-attribution') |  | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,21 +0,0 @@ | ||||||
| import {SubtleButton} from "../Base/SubtleButton"; |  | ||||||
| import Svg from "../../Svg"; |  | ||||||
| import Translations from "../i18n/Translations"; |  | ||||||
| import State from "../../State"; |  | ||||||
| import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource"; |  | ||||||
| import {Utils} from "../../Utils"; |  | ||||||
| import Combine from "../Base/Combine"; |  | ||||||
| 
 |  | ||||||
| export class ExportDataButton extends Combine { |  | ||||||
|     constructor() { |  | ||||||
|         const t = Translations.t.general.download |  | ||||||
|         const button = new SubtleButton(Svg.floppy_ui(), t.downloadGeojson.Clone().SetClass("font-bold")) |  | ||||||
|             .onClick(() => { |  | ||||||
|                 const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline) |  | ||||||
|                 const name = State.state.layoutToUse.data.id; |  | ||||||
|                 Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), `MapComplete_${name}_export_${new Date().toISOString().substr(0,19)}.geojson`); |  | ||||||
|             }) |  | ||||||
|          |  | ||||||
|         super([button, t.licenseInfo.Clone().SetClass("link-underline")]) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -10,7 +10,6 @@ import LayerConfig from "../../Customizations/JSON/LayerConfig"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import { Translation } from "../i18n/Translation"; | import { Translation } from "../i18n/Translation"; | ||||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | ||||||
| import Svg from "../../Svg"; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Shows the filter |  * Shows the filter | ||||||
|  | @ -27,63 +26,14 @@ export default class FilterView extends ScrollableFullScreen { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private static Generatecontent(): BaseUIElement { |   private static Generatecontent(): BaseUIElement { | ||||||
|     let filterPanel: BaseUIElement = new FixedUiElement(""); |     let filterPanel: BaseUIElement = new FixedUiElement("more stuff"); | ||||||
| 
 | 
 | ||||||
|     if (State.state.filteredLayers.data.length > 1) { |     if (State.state.filteredLayers.data.length > 1) { | ||||||
|       let activeLayers = State.state.filteredLayers; |       let layers = State.state.filteredLayers; | ||||||
| 
 |       console.log(layers); | ||||||
|       if (activeLayers === undefined) { |       filterPanel = new Combine(["layerssss", "<br/>", filterPanel]); | ||||||
|         throw "ActiveLayers should be defined..."; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|       const checkboxes: BaseUIElement[] = []; |  | ||||||
| 
 |  | ||||||
|       for (const layer of activeLayers.data) { |  | ||||||
|         const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem"; |  | ||||||
| 
 |  | ||||||
|         const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle); |  | ||||||
|         const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle( |  | ||||||
|           iconStyle |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         if (layer.layerDef.name === undefined) { |  | ||||||
|           continue; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const style = "display:flex;align-items:center;color:#007759"; |  | ||||||
| 
 |  | ||||||
|         const name: Translation = Translations.WT(layer.layerDef.name)?.Clone(); |  | ||||||
| 
 |  | ||||||
|         const styledNameChecked = name |  | ||||||
|           .Clone() |  | ||||||
|           .SetStyle("font-size:large;padding-left:1.25rem"); |  | ||||||
| 
 |  | ||||||
|         const styledNameUnChecked = name |  | ||||||
|           .Clone() |  | ||||||
|           .SetStyle("font-size:large;padding-left:1.25rem"); |  | ||||||
| 
 |  | ||||||
|         const layerChecked = new Combine([icon, styledNameChecked]).SetStyle( |  | ||||||
|           style |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         const layerNotChecked = new Combine([ |  | ||||||
|           iconUnselected, |  | ||||||
|           styledNameUnChecked, |  | ||||||
|         ]).SetStyle(style); |  | ||||||
| 
 |  | ||||||
|         checkboxes.push( |  | ||||||
|           new Toggle(layerChecked, layerNotChecked, layer.isDisplayed) |  | ||||||
|             .ToggleOnClick() |  | ||||||
|             .SetStyle("margin:0.3em;") |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       let combinedCheckboxes = new Combine(checkboxes); |  | ||||||
|       combinedCheckboxes.SetStyle("display:flex;flex-direction:column;"); |  | ||||||
| 
 |  | ||||||
|       filterPanel = new Combine([combinedCheckboxes]); |  | ||||||
| 
 |  | ||||||
|     return filterPanel; |     return filterPanel; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -2,12 +2,11 @@ import State from "../../State"; | ||||||
| import BackgroundSelector from "./BackgroundSelector"; | import BackgroundSelector from "./BackgroundSelector"; | ||||||
| import LayerSelection from "./LayerSelection"; | import LayerSelection from "./LayerSelection"; | ||||||
| import Combine from "../Base/Combine"; | import Combine from "../Base/Combine"; | ||||||
|  | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import Toggle from "../Input/Toggle"; |  | ||||||
| import {ExportDataButton} from "./ExportDataButton"; |  | ||||||
| 
 | 
 | ||||||
| export default class LayerControlPanel extends ScrollableFullScreen { | export default class LayerControlPanel extends ScrollableFullScreen { | ||||||
| 
 | 
 | ||||||
|  | @ -20,29 +19,22 @@ export default class LayerControlPanel extends ScrollableFullScreen { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static GeneratePanel() : BaseUIElement { |     private static GeneratePanel() : BaseUIElement { | ||||||
|         const elements: BaseUIElement[] = [] |         let layerControlPanel: BaseUIElement = new FixedUiElement(""); | ||||||
| 
 |  | ||||||
|         if (State.state.layoutToUse.data.enableBackgroundLayerSelection) { |         if (State.state.layoutToUse.data.enableBackgroundLayerSelection) { | ||||||
|             const backgroundSelector = new BackgroundSelector(); |             layerControlPanel = new BackgroundSelector(); | ||||||
|             backgroundSelector.SetStyle("margin:1em"); |             layerControlPanel.SetStyle("margin:1em"); | ||||||
|             backgroundSelector.onClick(() => { |             layerControlPanel.onClick(() => { | ||||||
|             }); |             }); | ||||||
|             elements.push(backgroundSelector) |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         elements.push(new Toggle( |         if (State.state.filteredLayers.data.length > 1) { | ||||||
|             new LayerSelection(State.state.filteredLayers), |             const layerSelection = new LayerSelection(State.state.filteredLayers); | ||||||
|             undefined, |             layerSelection.onClick(() => { | ||||||
|             State.state.filteredLayers.map(layers => layers.length > 1) |             }); | ||||||
|         )) |             layerControlPanel = new Combine([layerSelection, "<br/>", layerControlPanel]); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         elements.push(new Toggle( |         return layerControlPanel; | ||||||
|             new ExportDataButton(), |  | ||||||
|             undefined, |  | ||||||
|             State.state.featureSwitchEnableExport |  | ||||||
|         )) |  | ||||||
| 
 |  | ||||||
|         return new Combine(elements).SetClass("flex flex-col") |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -74,6 +74,7 @@ export default class LayerSelection extends Combine { | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         super(checkboxes) |         super(checkboxes) | ||||||
|         this.SetStyle("display:flex;flex-direction:column;") |         this.SetStyle("display:flex;flex-direction:column;") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -62,10 +62,6 @@ export default class MoreScreen extends Combine { | ||||||
|         let officialThemes = AllKnownLayouts.layoutsList |         let officialThemes = AllKnownLayouts.layoutsList | ||||||
| 
 | 
 | ||||||
|         let buttons = officialThemes.map((layout) => { |         let buttons = officialThemes.map((layout) => { | ||||||
|             if(layout === undefined){ |  | ||||||
|                 console.trace("Layout is undefined") |  | ||||||
|                 return undefined |  | ||||||
|             } |  | ||||||
|             const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass); |             const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass); | ||||||
|             if(layout.id === personal.id){ |             if(layout.id === personal.id){ | ||||||
|                 return new VariableUiElement( |                 return new VariableUiElement( | ||||||
|  |  | ||||||
|  | @ -16,10 +16,6 @@ import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
| import Toggle from "../Input/Toggle"; | import Toggle from "../Input/Toggle"; | ||||||
| import UserDetails from "../../Logic/Osm/OsmConnection"; | import UserDetails from "../../Logic/Osm/OsmConnection"; | ||||||
| import {Translation} from "../i18n/Translation"; | import {Translation} from "../i18n/Translation"; | ||||||
| import LocationInput from "../Input/LocationInput"; |  | ||||||
| import {InputElement} from "../Input/InputElement"; |  | ||||||
| import Loc from "../../Models/Loc"; |  | ||||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; |  | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
| * The SimpleAddUI is a single panel, which can have multiple states: | * The SimpleAddUI is a single panel, which can have multiple states: | ||||||
|  | @ -29,18 +25,14 @@ import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||||
| * - A 'read your unread messages before adding a point' | * - A 'read your unread messages before adding a point' | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| /*private*/ |  | ||||||
| interface PresetInfo { | interface PresetInfo { | ||||||
|     description: string | Translation, |     description: string | Translation, | ||||||
|     name: string | BaseUIElement, |     name: string | BaseUIElement, | ||||||
|     icon: () => BaseUIElement, |     icon: BaseUIElement, | ||||||
|     tags: Tag[], |     tags: Tag[], | ||||||
|     layerToAddTo: { |     layerToAddTo: { | ||||||
|         layerDef: LayerConfig, |         layerDef: LayerConfig, | ||||||
|         isDisplayed: UIEventSource<boolean> |         isDisplayed: UIEventSource<boolean> | ||||||
|     }, |  | ||||||
|     preciseInput?: { |  | ||||||
|         preferredBackground?: string |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -58,11 +50,13 @@ export default class SimpleAddUI extends Toggle { | ||||||
|         ]); |         ]); | ||||||
|          |          | ||||||
|          |          | ||||||
|  |          | ||||||
|         const selectedPreset = new UIEventSource<PresetInfo>(undefined); |         const selectedPreset = new UIEventSource<PresetInfo>(undefined); | ||||||
|         isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
 |         isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
 | ||||||
|          |          | ||||||
|         function createNewPoint(tags: any[], location: { lat: number, lon: number }) { |         function createNewPoint(tags: any[]){ | ||||||
|             let feature = State.state.changes.createElement(tags, location.lat, location.lon); |            const loc = State.state.LastClickLocation.data; | ||||||
|  |             let feature = State.state.changes.createElement(tags, loc.lat, loc.lon); | ||||||
|             State.state.selectedElement.setData(feature); |             State.state.selectedElement.setData(feature); | ||||||
|         } |         } | ||||||
|          |          | ||||||
|  | @ -74,8 +68,8 @@ export default class SimpleAddUI extends Toggle { | ||||||
|                         return presetsOverview |                         return presetsOverview | ||||||
|                     } |                     } | ||||||
|                     return SimpleAddUI.CreateConfirmButton(preset, |                     return SimpleAddUI.CreateConfirmButton(preset, | ||||||
|                         (tags, location) => { |                         tags => { | ||||||
|                             createNewPoint(tags, location) |                             createNewPoint(tags) | ||||||
|                             selectedPreset.setData(undefined) |                             selectedPreset.setData(undefined) | ||||||
|                         }, () => { |                         }, () => { | ||||||
|                             selectedPreset.setData(undefined) |                             selectedPreset.setData(undefined) | ||||||
|  | @ -109,46 +103,20 @@ export default class SimpleAddUI extends Toggle { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|     private static CreateConfirmButton(preset: PresetInfo, |     private static CreateConfirmButton(preset: PresetInfo, | ||||||
|                                        confirm: (tags: any[], location: { lat: number, lon: number }) => void, |                                        confirm: (tags: any[]) => void,  | ||||||
|                                        cancel: () => void): BaseUIElement { |                                        cancel: () => void): BaseUIElement { | ||||||
| 
 | 
 | ||||||
|         let location = State.state.LastClickLocation; |  | ||||||
|         let preciseInput: InputElement<Loc> = undefined |  | ||||||
|         if (preset.preciseInput !== undefined) { |  | ||||||
|             const locationSrc = new UIEventSource({ |  | ||||||
|                 lat: location.data.lat, |  | ||||||
|                 lon: location.data.lon, |  | ||||||
|                 zoom: 19 |  | ||||||
|             }); |  | ||||||
| 
 | 
 | ||||||
|             let backgroundLayer = undefined; |         const confirmButton = new SubtleButton(preset.icon, | ||||||
|             if(preset.preciseInput.preferredBackground){ |  | ||||||
|                backgroundLayer= AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground)) |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             preciseInput = new LocationInput({ |  | ||||||
|                 mapBackground: backgroundLayer, |  | ||||||
|                 centerLocation:locationSrc |  | ||||||
|                      |  | ||||||
|             }) |  | ||||||
|             preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;") |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         let confirmButton: BaseUIElement = new SubtleButton(preset.icon(), |  | ||||||
|             new Combine([ |             new Combine([ | ||||||
|                 Translations.t.general.add.addNew.Subs({category: preset.name}), |                 Translations.t.general.add.addNew.Subs({category: preset.name}), | ||||||
|                 Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert") |                 Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert") | ||||||
|             ]).SetClass("flex flex-col") |             ]).SetClass("flex flex-col") | ||||||
|         ).SetClass("font-bold break-words") |         ).SetClass("font-bold break-words") | ||||||
|             .onClick(() => { |             .onClick(() => confirm(preset.tags)); | ||||||
|                 confirm(preset.tags, (preciseInput?.GetValue() ?? location).data); |  | ||||||
|             }); |  | ||||||
| 
 | 
 | ||||||
|         if (preciseInput !== undefined) { |  | ||||||
|             confirmButton = new Combine([preciseInput, confirmButton]) |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         const openLayerControl =   |         const openLayerControl =   | ||||||
|             new SubtleButton( |             new SubtleButton( | ||||||
|  | @ -216,7 +184,7 @@ export default class SimpleAddUI extends Toggle { | ||||||
| 
 | 
 | ||||||
|         const tagInfo =SimpleAddUI.CreateTagInfoFor(preset, false); |         const tagInfo =SimpleAddUI.CreateTagInfoFor(preset, false); | ||||||
|         return new SubtleButton( |         return new SubtleButton( | ||||||
|             preset.icon(), |             preset.icon, | ||||||
|             new Combine([ |             new Combine([ | ||||||
|                 Translations.t.general.add.addNew.Subs({ |                 Translations.t.general.add.addNew.Subs({ | ||||||
|                     category: preset.name |                     category: preset.name | ||||||
|  | @ -241,15 +209,14 @@ export default class SimpleAddUI extends Toggle { | ||||||
|             for (const preset of presets) { |             for (const preset of presets) { | ||||||
| 
 | 
 | ||||||
|                 const tags = TagUtils.KVtoProperties(preset.tags ?? []); |                 const tags = TagUtils.KVtoProperties(preset.tags ?? []); | ||||||
|                 let icon:() => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html |                 let icon: BaseUIElement = layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html | ||||||
|                     .SetClass("w-12 h-12 block relative"); |                     .SetClass("w-12 h-12 block relative"); | ||||||
|                 const presetInfo: PresetInfo = { |                 const presetInfo: PresetInfo = { | ||||||
|                     tags: preset.tags, |                     tags: preset.tags, | ||||||
|                     layerToAddTo: layer, |                     layerToAddTo: layer, | ||||||
|                     name: preset.title, |                     name: preset.title, | ||||||
|                     description: preset.description, |                     description: preset.description, | ||||||
|                     icon: icon, |                     icon: icon | ||||||
|                     preciseInput: preset.preciseInput |  | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 const button = SimpleAddUI.CreatePresetSelectButton(presetInfo); |                 const button = SimpleAddUI.CreatePresetSelectButton(presetInfo); | ||||||
|  |  | ||||||
|  | @ -66,7 +66,6 @@ export default class DirectionInput extends InputElement<string> { | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         this.RegisterTriggers(element) |         this.RegisterTriggers(element) | ||||||
|         element.style.overflow = "hidden" |  | ||||||
| 
 | 
 | ||||||
|         return element; |         return element; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,35 +0,0 @@ | ||||||
| import {InputElement} from "./InputElement"; |  | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; |  | ||||||
| import BaseUIElement from "../BaseUIElement"; |  | ||||||
| import {Translation} from "../i18n/Translation"; |  | ||||||
| import {SubstitutedTranslation} from "../SubstitutedTranslation"; |  | ||||||
| 
 |  | ||||||
| export default class InputElementWrapper<T> extends InputElement<T> { |  | ||||||
|     public readonly IsSelected: UIEventSource<boolean>; |  | ||||||
|     private readonly _inputElement: InputElement<T>; |  | ||||||
|     private readonly _renderElement: BaseUIElement |  | ||||||
| 
 |  | ||||||
|     constructor(inputElement: InputElement<T>, translation: Translation, key: string, tags: UIEventSource<any>) { |  | ||||||
|         super() |  | ||||||
|         this._inputElement = inputElement; |  | ||||||
|         this.IsSelected = inputElement.IsSelected |  | ||||||
|         const mapping = new Map<string, BaseUIElement>() |  | ||||||
| 
 |  | ||||||
|         mapping.set(key, inputElement) |  | ||||||
| 
 |  | ||||||
|         this._renderElement = new SubstitutedTranslation(translation, tags, mapping) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     GetValue(): UIEventSource<T> { |  | ||||||
|         return this._inputElement.GetValue(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     IsValid(t: T): boolean { |  | ||||||
|         return this._inputElement.IsValid(t); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     protected InnerConstructElement(): HTMLElement { |  | ||||||
|         return this._renderElement.ConstructElement(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | @ -1,185 +0,0 @@ | ||||||
| import {InputElement} from "./InputElement"; |  | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; |  | ||||||
| import Combine from "../Base/Combine"; |  | ||||||
| import Svg from "../../Svg"; |  | ||||||
| import {Utils} from "../../Utils"; |  | ||||||
| import Loc from "../../Models/Loc"; |  | ||||||
| import {GeoOperations} from "../../Logic/GeoOperations"; |  | ||||||
| import DirectionInput from "./DirectionInput"; |  | ||||||
| import {RadioButton} from "./RadioButton"; |  | ||||||
| import {FixedInputElement} from "./FixedInputElement"; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Selects a length after clicking on the minimap, in meters |  | ||||||
|  */ |  | ||||||
| export default class LengthInput extends InputElement<string> { |  | ||||||
|     private readonly _location: UIEventSource<Loc>; |  | ||||||
| 
 |  | ||||||
|     public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); |  | ||||||
|     private readonly value: UIEventSource<string>; |  | ||||||
|     private background; |  | ||||||
| 
 |  | ||||||
|     constructor(mapBackground: UIEventSource<any>, |  | ||||||
|                 location: UIEventSource<Loc>, |  | ||||||
|                 value?: UIEventSource<string>) { |  | ||||||
|         super(); |  | ||||||
|         this._location = location; |  | ||||||
|         this.value = value ?? new UIEventSource<string>(undefined); |  | ||||||
|         this.background = mapBackground; |  | ||||||
|         this.SetClass("block") |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     GetValue(): UIEventSource<string> { |  | ||||||
|         return this.value; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     IsValid(str: string): boolean { |  | ||||||
|         const t = Number(str) |  | ||||||
|         return !isNaN(t) && t >= 0 && t <= 360; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     protected InnerConstructElement(): HTMLElement { |  | ||||||
|         const modeElement = new RadioButton([ |  | ||||||
|             new FixedInputElement("Measure", "measure"), |  | ||||||
|             new FixedInputElement("Move", "move") |  | ||||||
|         ]) |  | ||||||
|         // @ts-ignore
 |  | ||||||
|         let map = undefined |  | ||||||
|         if (!Utils.runningFromConsole) { |  | ||||||
|             map = DirectionInput.constructMinimap({ |  | ||||||
|                 background: this.background, |  | ||||||
|                 allowMoving: false, |  | ||||||
|                 location: this._location, |  | ||||||
|                 leafletOptions: { |  | ||||||
|                     tap: true |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|         } |  | ||||||
|         const element = new Combine([ |  | ||||||
|             new Combine([Svg.length_crosshair_svg().SetStyle( |  | ||||||
|                 `position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`) |  | ||||||
|             ]) |  | ||||||
|                 .SetClass("block length-crosshair-svg relative") |  | ||||||
|                 .SetStyle("z-index: 1000; visibility: hidden"), |  | ||||||
|             map?.SetClass("w-full h-full block absolute top-0 left-O overflow-hidden"), |  | ||||||
|         ]) |  | ||||||
|             .SetClass("relative block bg-white border border-black rounded-3xl overflow-hidden") |  | ||||||
|             .ConstructElement() |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         this.RegisterTriggers(element, map?.leafletMap) |  | ||||||
|         element.style.overflow = "hidden" |  | ||||||
|         element.style.display = "block" |  | ||||||
|          |  | ||||||
|       return element |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private RegisterTriggers(htmlElement: HTMLElement, leafletMap: UIEventSource<L.Map>) { |  | ||||||
| 
 |  | ||||||
|         let firstClickXY: [number, number] = undefined |  | ||||||
|         let lastClickXY: [number, number] = undefined |  | ||||||
|         const self = this; |  | ||||||
|          |  | ||||||
| 
 |  | ||||||
|         function onPosChange(x: number, y: number, isDown: boolean, isUp?: boolean) { |  | ||||||
|             if (x === undefined || y === undefined) { |  | ||||||
|                 // Touch end
 |  | ||||||
|                 firstClickXY = undefined; |  | ||||||
|                 lastClickXY = undefined; |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             const rect = htmlElement.getBoundingClientRect(); |  | ||||||
|             // From the central part of location
 |  | ||||||
|             const dx = x - rect.left; |  | ||||||
|             const dy = y - rect.top; |  | ||||||
|             if (isDown) { |  | ||||||
|                 if (lastClickXY === undefined && firstClickXY === undefined) { |  | ||||||
|                     firstClickXY = [dx, dy]; |  | ||||||
|                 } else if (firstClickXY !== undefined && lastClickXY === undefined) { |  | ||||||
|                     lastClickXY = [dx, dy] |  | ||||||
|                 } else if (firstClickXY !== undefined && lastClickXY !== undefined) { |  | ||||||
|                     // we measure again
 |  | ||||||
|                     firstClickXY = [dx, dy] |  | ||||||
|                     lastClickXY = undefined; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             if (isUp) { |  | ||||||
|                 const distance = Math.sqrt((dy - firstClickXY[1]) * (dy - firstClickXY[1]) + (dx - firstClickXY[0]) * (dx - firstClickXY[0])) |  | ||||||
|                 if (distance > 15) { |  | ||||||
|                     lastClickXY = [dx, dy] |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             } else if (lastClickXY !== undefined) { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             const measurementCrosshair = htmlElement.getElementsByClassName("length-crosshair-svg")[0] as HTMLElement |  | ||||||
|              |  | ||||||
|             const measurementCrosshairInner: HTMLElement = <HTMLElement>measurementCrosshair.firstChild |  | ||||||
|             if (firstClickXY === undefined) { |  | ||||||
|                 measurementCrosshair.style.visibility = "hidden" |  | ||||||
|             } else { |  | ||||||
|                 measurementCrosshair.style.visibility = "unset" |  | ||||||
|                 measurementCrosshair.style.left = firstClickXY[0] + "px"; |  | ||||||
|                 measurementCrosshair.style.top = firstClickXY[1] + "px" |  | ||||||
| 
 |  | ||||||
|                 const angle = 180 * Math.atan2(firstClickXY[1] - dy, firstClickXY[0] - dx) / Math.PI; |  | ||||||
|                 const angleGeo = (angle + 270) % 360 |  | ||||||
|                 measurementCrosshairInner.style.transform = `rotate(${angleGeo}deg)`; |  | ||||||
| 
 |  | ||||||
|                 const distance = Math.sqrt((dy - firstClickXY[1]) * (dy - firstClickXY[1]) + (dx - firstClickXY[0]) * (dx - firstClickXY[0])) |  | ||||||
|                 measurementCrosshairInner.style.width = (distance * 2) + "px" |  | ||||||
|                 measurementCrosshairInner.style.marginLeft = -distance + "px" |  | ||||||
|                 measurementCrosshairInner.style.marginTop = -distance + "px" |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                 const leaflet = leafletMap?.data |  | ||||||
|                 if (leaflet) { |  | ||||||
|                     const first = leaflet.layerPointToLatLng(firstClickXY) |  | ||||||
|                     const last = leaflet.layerPointToLatLng([dx, dy]) |  | ||||||
|                     const geoDist = Math.floor(GeoOperations.distanceBetween([first.lng, first.lat], [last.lng, last.lat]) * 100000) / 100 |  | ||||||
|                     self.value.setData("" + geoDist) |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         htmlElement.ontouchstart = (ev: TouchEvent) => { |  | ||||||
|             onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, true); |  | ||||||
|             ev.preventDefault(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         htmlElement.ontouchmove = (ev: TouchEvent) => { |  | ||||||
|             onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, false); |  | ||||||
|             ev.preventDefault(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         htmlElement.ontouchend = (ev: TouchEvent) => { |  | ||||||
|             onPosChange(undefined, undefined, false, true); |  | ||||||
|             ev.preventDefault(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         htmlElement.onmousedown = (ev: MouseEvent) => { |  | ||||||
|             onPosChange(ev.clientX, ev.clientY, true); |  | ||||||
|             ev.preventDefault(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         htmlElement.onmouseup = (ev) => { |  | ||||||
|             onPosChange(ev.clientX, ev.clientY, false, true); |  | ||||||
|             ev.preventDefault(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         htmlElement.onmousemove = (ev: MouseEvent) => { |  | ||||||
|             onPosChange(ev.clientX, ev.clientY, false); |  | ||||||
|             ev.preventDefault(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | @ -1,76 +0,0 @@ | ||||||
| import {InputElement} from "./InputElement"; |  | ||||||
| import Loc from "../../Models/Loc"; |  | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; |  | ||||||
| import Minimap from "../Base/Minimap"; |  | ||||||
| import BaseLayer from "../../Models/BaseLayer"; |  | ||||||
| import Combine from "../Base/Combine"; |  | ||||||
| import Svg from "../../Svg"; |  | ||||||
| import State from "../../State"; |  | ||||||
| 
 |  | ||||||
| export default class LocationInput extends InputElement<Loc> { |  | ||||||
| 
 |  | ||||||
|     IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); |  | ||||||
|     private _centerLocation: UIEventSource<Loc>; |  | ||||||
|     private readonly mapBackground : UIEventSource<BaseLayer>; |  | ||||||
| 
 |  | ||||||
|     constructor(options?: { |  | ||||||
|         mapBackground?: UIEventSource<BaseLayer>, |  | ||||||
|         centerLocation?: UIEventSource<Loc>, |  | ||||||
|     }) { |  | ||||||
|         super(); |  | ||||||
|         options = options ?? {} |  | ||||||
|         options.centerLocation = options.centerLocation ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1}) |  | ||||||
|         this._centerLocation = options.centerLocation; |  | ||||||
| 
 |  | ||||||
|         this.mapBackground = options.mapBackground ?? State.state.backgroundLayer |  | ||||||
|         this.SetClass("block h-full") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     GetValue(): UIEventSource<Loc> { |  | ||||||
|         return this._centerLocation; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     IsValid(t: Loc): boolean { |  | ||||||
|         return t !== undefined; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     protected InnerConstructElement(): HTMLElement { |  | ||||||
|         const map = new Minimap( |  | ||||||
|             { |  | ||||||
|                 location: this._centerLocation, |  | ||||||
|                 background: this.mapBackground |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|         map.leafletMap.addCallbackAndRunD(leaflet => { |  | ||||||
|             console.log(leaflet.getBounds(), leaflet.getBounds().pad(0.15)) |  | ||||||
|             leaflet.setMaxBounds( |  | ||||||
|                 leaflet.getBounds().pad(0.15) |  | ||||||
|             ) |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         this.mapBackground.map(layer => { |  | ||||||
| 
 |  | ||||||
|             const leaflet = map.leafletMap.data |  | ||||||
|             if (leaflet === undefined || layer === undefined) { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             leaflet.setMaxZoom(layer.max_zoom) |  | ||||||
|             leaflet.setMinZoom(layer.max_zoom - 3) |  | ||||||
|             leaflet.setZoom(layer.max_zoom - 1) |  | ||||||
| 
 |  | ||||||
|         }, [map.leafletMap]) |  | ||||||
|         return new Combine([ |  | ||||||
|             new Combine([ |  | ||||||
|                 Svg.crosshair_empty_ui() |  | ||||||
|                     .SetClass("block relative") |  | ||||||
|                     .SetStyle("left: -1.25rem; top: -1.25rem; width: 2.5rem; height: 2.5rem") |  | ||||||
|             ]).SetClass("block w-0 h-0 z-10 relative") |  | ||||||
|                 .SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%"), |  | ||||||
|             map |  | ||||||
|                 .SetClass("z-0 relative block w-full h-full bg-gray-100") |  | ||||||
| 
 |  | ||||||
|         ]).ConstructElement(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | @ -103,7 +103,7 @@ export class RadioButton<T> extends InputElement<T> { | ||||||
|             const block = document.createElement("div") |             const block = document.createElement("div") | ||||||
|             block.appendChild(input) |             block.appendChild(input) | ||||||
|             block.appendChild(label) |             block.appendChild(label) | ||||||
|             block.classList.add("flex","w-full","border", "rounded-3xl", "border-gray-400","m-1") |             block.classList.add("flex","w-full","border", "rounded-full", "border-gray-400","m-1") | ||||||
|             wrappers.push(block) |             wrappers.push(block) | ||||||
| 
 | 
 | ||||||
|             form.appendChild(block) |             form.appendChild(block) | ||||||
|  |  | ||||||
|  | @ -36,11 +36,11 @@ export class TextField extends InputElement<string> { | ||||||
|         this.SetClass("form-text-field") |         this.SetClass("form-text-field") | ||||||
|         let inputEl: HTMLElement |         let inputEl: HTMLElement | ||||||
|         if (options.htmlType === "area") { |         if (options.htmlType === "area") { | ||||||
|             this.SetClass("w-full box-border max-w-full") |  | ||||||
|             const el = document.createElement("textarea") |             const el = document.createElement("textarea") | ||||||
|             el.placeholder = placeholder |             el.placeholder = placeholder | ||||||
|             el.rows = options.textAreaRows |             el.rows = options.textAreaRows | ||||||
|             el.cols = 50 |             el.cols = 50 | ||||||
|  |             el.style.cssText = "max-width: 100%; width: 100%; box-sizing: border-box" | ||||||
|             inputEl = el; |             inputEl = el; | ||||||
|         } else { |         } else { | ||||||
|             const el = document.createElement("input") |             const el = document.createElement("input") | ||||||
|  |  | ||||||
|  | @ -13,8 +13,6 @@ import {Utils} from "../../Utils"; | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc"; | ||||||
| import {Unit} from "../../Customizations/JSON/Denomination"; | import {Unit} from "../../Customizations/JSON/Denomination"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import LengthInput from "./LengthInput"; |  | ||||||
| import {GeoOperations} from "../../Logic/GeoOperations"; |  | ||||||
| 
 | 
 | ||||||
| interface TextFieldDef { | interface TextFieldDef { | ||||||
|     name: string, |     name: string, | ||||||
|  | @ -23,16 +21,14 @@ interface TextFieldDef { | ||||||
|     reformat?: ((s: string, country?: () => string) => string), |     reformat?: ((s: string, country?: () => string) => string), | ||||||
|     inputHelper?: (value: UIEventSource<string>, options?: { |     inputHelper?: (value: UIEventSource<string>, options?: { | ||||||
|         location: [number, number], |         location: [number, number], | ||||||
|         mapBackgroundLayer?: UIEventSource<any>, |         mapBackgroundLayer?: UIEventSource<any> | ||||||
|         args: (string | number | boolean)[] |  | ||||||
|         feature?: any |  | ||||||
|     }) => InputElement<string>, |     }) => InputElement<string>, | ||||||
|  | 
 | ||||||
|     inputmode?: string |     inputmode?: string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default class ValidatedTextField { | export default class ValidatedTextField { | ||||||
| 
 | 
 | ||||||
|     public static bestLayerAt: (location: UIEventSource<Loc>, preferences: UIEventSource<string[]>) => any |  | ||||||
| 
 | 
 | ||||||
|     public static tpList: TextFieldDef[] = [ |     public static tpList: TextFieldDef[] = [ | ||||||
|         ValidatedTextField.tp( |         ValidatedTextField.tp( | ||||||
|  | @ -67,83 +63,6 @@ export default class ValidatedTextField { | ||||||
|                 return [year, month, day].join('-'); |                 return [year, month, day].join('-'); | ||||||
|             }, |             }, | ||||||
|             (value) => new SimpleDatePicker(value)), |             (value) => new SimpleDatePicker(value)), | ||||||
|         ValidatedTextField.tp( |  | ||||||
|             "direction", |  | ||||||
|             "A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)", |  | ||||||
|             (str) => { |  | ||||||
|                 str = "" + str; |  | ||||||
|                 return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360 |  | ||||||
|             }, str => str, |  | ||||||
|             (value, options) => { |  | ||||||
|                 const args = options.args ?? [] |  | ||||||
|                 let zoom = 19 |  | ||||||
|                 if (args[0]) { |  | ||||||
|                     zoom = Number(args[0]) |  | ||||||
|                     if (isNaN(zoom)) { |  | ||||||
|                         throw "Invalid zoom level for argument at 'length'-input" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 const location = new UIEventSource<Loc>({ |  | ||||||
|                     lat: options.location[0], |  | ||||||
|                     lon: options.location[1], |  | ||||||
|                     zoom: zoom |  | ||||||
|                 }) |  | ||||||
|                 if (args[1]) { |  | ||||||
|                     // We have a prefered map!
 |  | ||||||
|                     options.mapBackgroundLayer = ValidatedTextField.bestLayerAt( |  | ||||||
|                         location, new UIEventSource<string[]>(args[1].split(",")) |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|                 const di = new DirectionInput(options.mapBackgroundLayer, location, value) |  | ||||||
|                 di.SetStyle("height: 20rem;"); |  | ||||||
| 
 |  | ||||||
|                 return di; |  | ||||||
|             }, |  | ||||||
|             "numeric" |  | ||||||
|         ), |  | ||||||
|         ValidatedTextField.tp( |  | ||||||
|             "length", |  | ||||||
|             "A geographical length in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma seperated) ], e.g. `[\"21\", \"map,photo\"]", |  | ||||||
|             (str) => { |  | ||||||
|                 const t = Number(str) |  | ||||||
|                 return !isNaN(t) |  | ||||||
|             }, |  | ||||||
|             str => str, |  | ||||||
|             (value, options) => { |  | ||||||
|                 const args = options.args ?? [] |  | ||||||
|                 let zoom = 19 |  | ||||||
|                 if (args[0]) { |  | ||||||
|                     zoom = Number(args[0]) |  | ||||||
|                     if (isNaN(zoom)) { |  | ||||||
|                         throw "Invalid zoom level for argument at 'length'-input" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 // Bit of a hack: we project the centerpoint to the closes point on the road - if available
 |  | ||||||
|                 if(options.feature){ |  | ||||||
|                     const lonlat: [number, number] = [...options.location] |  | ||||||
|                     lonlat.reverse() |  | ||||||
|                     options.location = <[number,number]> GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates |  | ||||||
|                     options.location.reverse() |  | ||||||
|                 } |  | ||||||
|                 options.feature |  | ||||||
|                  |  | ||||||
|                 const location = new UIEventSource<Loc>({ |  | ||||||
|                     lat: options.location[0], |  | ||||||
|                     lon: options.location[1], |  | ||||||
|                     zoom: zoom |  | ||||||
|                 }) |  | ||||||
|                 if (args[1]) { |  | ||||||
|                     // We have a prefered map!
 |  | ||||||
|                     options.mapBackgroundLayer = ValidatedTextField.bestLayerAt( |  | ||||||
|                         location, new UIEventSource<string[]>(args[1].split(",")) |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|                 const li = new LengthInput(options.mapBackgroundLayer, location, value) |  | ||||||
|                 li.SetStyle("height: 20rem;") |  | ||||||
|                 return li; |  | ||||||
|             } |  | ||||||
|         ), |  | ||||||
|         ValidatedTextField.tp( |         ValidatedTextField.tp( | ||||||
|             "wikidata", |             "wikidata", | ||||||
|             "A wikidata identifier, e.g. Q42", |             "A wikidata identifier, e.g. Q42", | ||||||
|  | @ -194,6 +113,22 @@ export default class ValidatedTextField { | ||||||
|             undefined, |             undefined, | ||||||
|             undefined, |             undefined, | ||||||
|             "numeric"), |             "numeric"), | ||||||
|  |         ValidatedTextField.tp( | ||||||
|  |             "direction", | ||||||
|  |             "A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)", | ||||||
|  |             (str) => { | ||||||
|  |                 str = "" + str; | ||||||
|  |                 return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360 | ||||||
|  |             }, str => str, | ||||||
|  |             (value, options) => { | ||||||
|  |                 return new DirectionInput(options.mapBackgroundLayer , new UIEventSource<Loc>({ | ||||||
|  |                     lat: options.location[0], | ||||||
|  |                     lon: options.location[1], | ||||||
|  |                     zoom: 19 | ||||||
|  |                 }),value); | ||||||
|  |             }, | ||||||
|  |             "numeric" | ||||||
|  |         ), | ||||||
|         ValidatedTextField.tp( |         ValidatedTextField.tp( | ||||||
|             "float", |             "float", | ||||||
|             "A decimal", |             "A decimal", | ||||||
|  | @ -287,7 +222,6 @@ export default class ValidatedTextField { | ||||||
|      * {string (typename) --> TextFieldDef} |      * {string (typename) --> TextFieldDef} | ||||||
|      */ |      */ | ||||||
|     public static AllTypes = ValidatedTextField.allTypesDict(); |     public static AllTypes = ValidatedTextField.allTypesDict(); | ||||||
| 
 |  | ||||||
|     public static InputForType(type: string, options?: { |     public static InputForType(type: string, options?: { | ||||||
|         placeholder?: string | BaseUIElement, |         placeholder?: string | BaseUIElement, | ||||||
|         value?: UIEventSource<string>, |         value?: UIEventSource<string>, | ||||||
|  | @ -299,9 +233,7 @@ export default class ValidatedTextField { | ||||||
|         country?: () => string, |         country?: () => string, | ||||||
|         location?: [number /*lat*/, number /*lon*/], |         location?: [number /*lat*/, number /*lon*/], | ||||||
|         mapBackgroundLayer?: UIEventSource<any>, |         mapBackgroundLayer?: UIEventSource<any>, | ||||||
|         unit?: Unit, |         unit?: Unit | ||||||
|         args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
 |  | ||||||
|         feature?: any |  | ||||||
|     }): InputElement<string> { |     }): InputElement<string> { | ||||||
|         options = options ?? {}; |         options = options ?? {}; | ||||||
|         options.placeholder = options.placeholder ?? type; |         options.placeholder = options.placeholder ?? type; | ||||||
|  | @ -350,7 +282,7 @@ export default class ValidatedTextField { | ||||||
|                 }) |                 }) | ||||||
|             ) |             ) | ||||||
|             unitDropDown.GetValue().setData(unit.defaultDenom) |             unitDropDown.GetValue().setData(unit.defaultDenom) | ||||||
|             unitDropDown.SetClass("w-min") |             unitDropDown.SetStyle("width: min-content") | ||||||
| 
 | 
 | ||||||
|             input = new CombinedInputElement( |             input = new CombinedInputElement( | ||||||
|                 input, |                 input, | ||||||
|  | @ -360,7 +292,8 @@ export default class ValidatedTextField { | ||||||
|                 (valueWithDenom: string) => { |                 (valueWithDenom: string) => { | ||||||
|                     // Take the value from OSM and feed it into the textfield and the dropdown
 |                     // Take the value from OSM and feed it into the textfield and the dropdown
 | ||||||
|                     const withDenom = unit.findDenomination(valueWithDenom); |                     const withDenom = unit.findDenomination(valueWithDenom); | ||||||
|                     if (withDenom === undefined) { |                     if(withDenom === undefined) | ||||||
|  |                     { | ||||||
|                         // Not a valid value at all - we give it undefined and leave the details up to the other elements
 |                         // Not a valid value at all - we give it undefined and leave the details up to the other elements
 | ||||||
|                         return [undefined, undefined] |                         return [undefined, undefined] | ||||||
|                     } |                     } | ||||||
|  | @ -375,9 +308,8 @@ export default class ValidatedTextField { | ||||||
|         if (tp.inputHelper) { |         if (tp.inputHelper) { | ||||||
|             const helper =  tp.inputHelper(input.GetValue(), { |             const helper =  tp.inputHelper(input.GetValue(), { | ||||||
|                 location: options.location, |                 location: options.location, | ||||||
|                 mapBackgroundLayer: options.mapBackgroundLayer, |                 mapBackgroundLayer: options.mapBackgroundLayer | ||||||
|                 args: options.args, | 
 | ||||||
|                 feature: options.feature |  | ||||||
|             }) |             }) | ||||||
|             input = new CombinedInputElement(input, helper, |             input = new CombinedInputElement(input, helper, | ||||||
|                 (a, _) => a, // We can ignore b, as they are linked earlier
 |                 (a, _) => a, // We can ignore b, as they are linked earlier
 | ||||||
|  | @ -386,7 +318,6 @@ export default class ValidatedTextField { | ||||||
|         } |         } | ||||||
|         return input; |         return input; | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     public static HelpText(): string { |     public static HelpText(): string { | ||||||
|         const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n") |         const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n") | ||||||
|         return "# Available types for text fields\n\nThe listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them\n\n" + explanations |         return "# Available types for text fields\n\nThe listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them\n\n" + explanations | ||||||
|  | @ -398,9 +329,7 @@ export default class ValidatedTextField { | ||||||
|                       reformat?: ((s: string, country?: () => string) => string), |                       reformat?: ((s: string, country?: () => string) => string), | ||||||
|                       inputHelper?: (value: UIEventSource<string>, options?: { |                       inputHelper?: (value: UIEventSource<string>, options?: { | ||||||
|                           location: [number, number], |                           location: [number, number], | ||||||
|                           mapBackgroundLayer: UIEventSource<any>, |                           mapBackgroundLayer: UIEventSource<any> | ||||||
|                           args: string[], |  | ||||||
|                           feature: any |  | ||||||
|                       }) => InputElement<string>, |                       }) => InputElement<string>, | ||||||
|                       inputmode?: string): TextFieldDef { |                       inputmode?: string): TextFieldDef { | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { | ||||||
|             .SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2"); |             .SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2"); | ||||||
|         const titleIcons = new Combine( |         const titleIcons = new Combine( | ||||||
|             layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon, |             layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon, | ||||||
|                 "block w-8 h-8 align-baseline box-content sm:p-0.5", "width: 2rem;") |                 "block w-8 h-8 align-baseline box-content sm:p-0.5") | ||||||
|             )) |             )) | ||||||
|             .SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") |             .SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -40,7 +40,7 @@ export default class TagRenderingAnswer extends VariableUiElement { | ||||||
|            return undefined; |            return undefined; | ||||||
|                 }).map((element : BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle))) |                 }).map((element : BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle))) | ||||||
| 
 | 
 | ||||||
|         this.SetClass("flex items-center flex-row text-lg link-underline") |         this.SetClass("flex items-center flex-row text-lg link-underline tag-renering-answer") | ||||||
|         this.SetStyle("word-wrap: anywhere;"); |         this.SetStyle("word-wrap: anywhere;"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -24,7 +24,6 @@ import {TagUtils} from "../../Logic/Tags/TagUtils"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import {DropDown} from "../Input/DropDown"; | import {DropDown} from "../Input/DropDown"; | ||||||
| import {Unit} from "../../Customizations/JSON/Denomination"; | import {Unit} from "../../Customizations/JSON/Denomination"; | ||||||
| import InputElementWrapper from "../Input/InputElementWrapper"; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Shows the question element. |  * Shows the question element. | ||||||
|  | @ -129,7 +128,7 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|             } |             } | ||||||
|             return Utils.NoNull(configuration.mappings?.map((m,i) => excludeIndex === i ? undefined:  m.ifnot)) |             return Utils.NoNull(configuration.mappings?.map((m,i) => excludeIndex === i ? undefined:  m.ifnot)) | ||||||
|         } |         } | ||||||
|         const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource); |         const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource.data); | ||||||
|         const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0 |         const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0 | ||||||
| 
 | 
 | ||||||
|         if (mappings.length < 8 || configuration.multiAnswer || hasImages) { |         if (mappings.length < 8 || configuration.multiAnswer || hasImages) { | ||||||
|  | @ -290,7 +289,7 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|             (t0, t1) => t1.isEquivalent(t0)); |             (t0, t1) => t1.isEquivalent(t0)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource<any>): InputElement<TagsFilter> { |     private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tagsData: any): InputElement<TagsFilter> { | ||||||
|         const freeform = configuration.freeform; |         const freeform = configuration.freeform; | ||||||
|         if (freeform === undefined) { |         if (freeform === undefined) { | ||||||
|             return undefined; |             return undefined; | ||||||
|  | @ -329,35 +328,21 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|             return undefined; |             return undefined; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const tagsData = tags.data; |         let input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, { | ||||||
|         const feature = State.state.allElements.ContainingFeatures.get(tagsData.id) |  | ||||||
|         const input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, { |  | ||||||
|             isValid: (str) => (str.length <= 255), |             isValid: (str) => (str.length <= 255), | ||||||
|             country: () => tagsData._country, |             country: () => tagsData._country, | ||||||
|             location: [tagsData._lat, tagsData._lon], |             location: [tagsData._lat, tagsData._lon], | ||||||
|             mapBackgroundLayer: State.state.backgroundLayer, |             mapBackgroundLayer: State.state.backgroundLayer, | ||||||
|             unit: applicableUnit, |             unit: applicableUnit | ||||||
|             args: configuration.freeform.helperArgs, |  | ||||||
|             feature: feature |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         input.GetValue().setData(tagsData[freeform.key] ?? freeform.default); |         input.GetValue().setData(tagsData[configuration.freeform.key]); | ||||||
| 
 | 
 | ||||||
|         let inputTagsFilter : InputElement<TagsFilter> = new InputElementMap( |         return new InputElementMap( | ||||||
|             input, (a, b) => a === b || (a?.isEquivalent(b) ?? false), |             input, (a, b) => a === b || (a?.isEquivalent(b) ?? false), | ||||||
|             pickString, toString |             pickString, toString | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         if(freeform.inline){ |  | ||||||
|              |  | ||||||
|             inputTagsFilter.SetClass("w-16-imp") |  | ||||||
|             inputTagsFilter = new InputElementWrapper(inputTagsFilter, configuration.render, freeform.key, tags) |  | ||||||
|             inputTagsFilter.SetClass("block") |  | ||||||
|              |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         return inputTagsFilter; |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -80,7 +80,9 @@ export default class ShowDataLayer { | ||||||
| 
 | 
 | ||||||
|             if (zoomToFeatures) { |             if (zoomToFeatures) { | ||||||
|                 try { |                 try { | ||||||
|                     mp.fitBounds(geoLayer.getBounds(), {animate: false}) | 
 | ||||||
|  |                     mp.fitBounds(geoLayer.getBounds()) | ||||||
|  | 
 | ||||||
|                 } catch (e) { |                 } catch (e) { | ||||||
|                     console.error(e) |                     console.error(e) | ||||||
|                 } |                 } | ||||||
|  | @ -146,9 +148,7 @@ export default class ShowDataLayer { | ||||||
|         const popup = L.popup({ |         const popup = L.popup({ | ||||||
|             autoPan: true, |             autoPan: true, | ||||||
|             closeOnEscapeKey: true, |             closeOnEscapeKey: true, | ||||||
|             closeButton: false, |             closeButton: false | ||||||
|             autoPanPaddingTopLeft: [15,15], |  | ||||||
|              |  | ||||||
|         }, leafletLayer); |         }, leafletLayer); | ||||||
| 
 | 
 | ||||||
|         leafletLayer.bindPopup(popup); |         leafletLayer.bindPopup(popup); | ||||||
|  |  | ||||||
|  | @ -39,8 +39,7 @@ export default class SpecialVisualizations { | ||||||
|     static constructMiniMap: (options?: { |     static constructMiniMap: (options?: { | ||||||
|         background?: UIEventSource<BaseLayer>, |         background?: UIEventSource<BaseLayer>, | ||||||
|         location?: UIEventSource<Loc>, |         location?: UIEventSource<Loc>, | ||||||
|         allowMoving?: boolean, |         allowMoving?: boolean | ||||||
|         leafletOptions?: any |  | ||||||
|     }) => BaseUIElement; |     }) => BaseUIElement; | ||||||
|     static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any; |     static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any; | ||||||
|     public static specialVisualizations: SpecialVisualization[] = |     public static specialVisualizations: SpecialVisualization[] = | ||||||
|  | @ -370,6 +369,7 @@ export default class SpecialVisualizations { | ||||||
|                                 if (unit === undefined) { |                                 if (unit === undefined) { | ||||||
|                                     return value; |                                     return value; | ||||||
|                                 } |                                 } | ||||||
|  | 
 | ||||||
|                                 return unit.asHumanLongValue(value); |                                 return unit.asHumanLongValue(value); | ||||||
| 
 | 
 | ||||||
|                             }, |                             }, | ||||||
|  | @ -379,7 +379,6 @@ export default class SpecialVisualizations { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|         ] |         ] | ||||||
|      |  | ||||||
|     static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage(); |     static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage(); | ||||||
|     private static GenHelpMessage() { |     private static GenHelpMessage() { | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,43 +7,19 @@ import SpecialVisualizations, {SpecialVisualization} from "./SpecialVisualizatio | ||||||
| import {Utils} from "../Utils"; | import {Utils} from "../Utils"; | ||||||
| import {VariableUiElement} from "./Base/VariableUIElement"; | import {VariableUiElement} from "./Base/VariableUIElement"; | ||||||
| import Combine from "./Base/Combine"; | import Combine from "./Base/Combine"; | ||||||
| import BaseUIElement from "./BaseUIElement"; |  | ||||||
| 
 | 
 | ||||||
| export class SubstitutedTranslation extends VariableUiElement { | export class SubstitutedTranslation extends VariableUiElement { | ||||||
| 
 | 
 | ||||||
|     public constructor( |     public constructor( | ||||||
|         translation: Translation, |         translation: Translation, | ||||||
|         tagsSource: UIEventSource<any>, |         tagsSource: UIEventSource<any>) { | ||||||
|         mapping: Map<string, BaseUIElement> = undefined) { |  | ||||||
| 
 |  | ||||||
|         const extraMappings: SpecialVisualization[] = []; |  | ||||||
| 
 |  | ||||||
|         mapping?.forEach((value, key) => { |  | ||||||
|             console.log("KV:", key, value) |  | ||||||
|             extraMappings.push( |  | ||||||
|                 { |  | ||||||
|                     funcName: key, |  | ||||||
|                     constr: (() => { |  | ||||||
|                         return value |  | ||||||
|                     }), |  | ||||||
|                     docs: "Dynamically injected input element", |  | ||||||
|                     args: [], |  | ||||||
|                     example: "" |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         super( |         super( | ||||||
|             Locale.language.map(language => { |             Locale.language.map(language => { | ||||||
|                 let txt = translation.textFor(language); |                 const txt = translation.textFor(language) | ||||||
|                 if (txt === undefined) { |                 if (txt === undefined) { | ||||||
|                     return undefined |                     return undefined | ||||||
|                 } |                 } | ||||||
|                 mapping?.forEach((_, key) => { |                 return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt).map( | ||||||
|                     txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`) |  | ||||||
|                 }) |  | ||||||
| 
 |  | ||||||
|                 return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map( |  | ||||||
|                     proto => { |                     proto => { | ||||||
|                         if (proto.fixed !== undefined) { |                         if (proto.fixed !== undefined) { | ||||||
|                             return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags))); |                             return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags))); | ||||||
|  | @ -60,35 +36,30 @@ export class SubstitutedTranslation extends VariableUiElement { | ||||||
|             }) |             }) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         this.SetClass("w-full") |         this.SetClass("w-full") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     public static ExtractSpecialComponents(template: string, extraMappings: SpecialVisualization[] = []): { |     public static ExtractSpecialComponents(template: string): { | ||||||
|         fixed?: string, |         fixed?: string, special?: { | ||||||
|         special?: { |  | ||||||
|             func: SpecialVisualization, |             func: SpecialVisualization, | ||||||
|             args: string[], |             args: string[], | ||||||
|             style: string |             style: string | ||||||
|         } |         } | ||||||
|     }[] { |     }[] { | ||||||
| 
 | 
 | ||||||
|         if (extraMappings.length > 0) { |         for (const knownSpecial of SpecialVisualizations.specialVisualizations) { | ||||||
| 
 |  | ||||||
|             console.log("Extra mappings are", extraMappings) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (const knownSpecial of SpecialVisualizations.specialVisualizations.concat(extraMappings)) { |  | ||||||
| 
 | 
 | ||||||
|             // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
 |             // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
 | ||||||
|             const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`); |             const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`); | ||||||
|             if (matched != null) { |             if (matched != null) { | ||||||
| 
 | 
 | ||||||
|                 // We found a special component that should be brought to live
 |                 // We found a special component that should be brought to live
 | ||||||
|                 const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1], extraMappings); |                 const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1]); | ||||||
|                 const argument = matched[2].trim(); |                 const argument = matched[2].trim(); | ||||||
|                 const style = matched[3]?.substring(1) ?? "" |                 const style = matched[3]?.substring(1) ?? "" | ||||||
|                 const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4], extraMappings); |                 const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4]); | ||||||
|                 const args = knownSpecial.args.map(arg => arg.defaultValue ?? ""); |                 const args = knownSpecial.args.map(arg => arg.defaultValue ?? ""); | ||||||
|                 if (argument.length > 0) { |                 if (argument.length > 0) { | ||||||
|                     const realArgs = argument.split(",").map(str => str.trim()); |                     const realArgs = argument.split(",").map(str => str.trim()); | ||||||
|  | @ -102,13 +73,11 @@ export class SubstitutedTranslation extends VariableUiElement { | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 let element; |                 let element; | ||||||
|                 element = { |                 element =  {special:{ | ||||||
|                     special: { |  | ||||||
|                     args: args, |                     args: args, | ||||||
|                     style: style, |                     style: style, | ||||||
|                     func: knownSpecial |                     func: knownSpecial | ||||||
|                     } |                 }} | ||||||
|                 } |  | ||||||
|                 return [...partBefore, element, ...partAfter] |                 return [...partBefore, element, ...partAfter] | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
							
								
								
									
										24
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										24
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -1,5 +1,4 @@ | ||||||
| import * as colors from "./assets/colors.json" | import * as colors from "./assets/colors.json" | ||||||
| import {TileRange} from "./Models/TileRange"; |  | ||||||
| 
 | 
 | ||||||
| export class Utils { | export class Utils { | ||||||
| 
 | 
 | ||||||
|  | @ -359,12 +358,9 @@ export class Utils { | ||||||
|      * @param contents |      * @param contents | ||||||
|      * @param fileName |      * @param fileName | ||||||
|      */ |      */ | ||||||
|     public static offerContentsAsDownloadableFile(contents: string | Blob, fileName: string = "download.txt") { |     public static offerContentsAsDownloadableFile(contents: string, fileName: string = "download.txt") { | ||||||
|         const element = document.createElement("a"); |         const element = document.createElement("a"); | ||||||
|         let file; |         const file = new Blob([contents], {type: 'text/plain'}); | ||||||
|         if(typeof(contents) === "string"){ |  | ||||||
|             file = new Blob([contents], {type: 'text/plain'}); |  | ||||||
|         }else {file = contents;} |  | ||||||
|         element.href = URL.createObjectURL(file); |         element.href = URL.createObjectURL(file); | ||||||
|         element.download = fileName; |         element.download = fileName; | ||||||
|         document.body.appendChild(element); // Required for this to work in FireFox
 |         document.body.appendChild(element); // Required for this to work in FireFox
 | ||||||
|  | @ -451,12 +447,14 @@ export class Utils { | ||||||
|             b: parseInt(hex.substr(5, 2), 16), |             b: parseInt(hex.substr(5, 2), 16), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     public static setDefaults(options, defaults){ |  | ||||||
|         for (let key in defaults){ |  | ||||||
|             if (!(key in options)) options[key] = defaults[key]; |  | ||||||
|         } |  | ||||||
|         return options; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface TileRange { | ||||||
|  |     xstart: number, | ||||||
|  |     ystart: number, | ||||||
|  |     xend: number, | ||||||
|  |     yend: number, | ||||||
|  |     total: number, | ||||||
|  |     zoomlevel: number | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| { | { | ||||||
|   "id": "parking", |   "id": "parking", | ||||||
|   "name": { |   "name": { | ||||||
|     "nl": "Parking" |     "nl": "parking" | ||||||
|   }, |   }, | ||||||
|   "minzoom": 12, |   "minzoom": 12, | ||||||
|   "source": { |   "source": { | ||||||
|  | @ -25,13 +25,13 @@ | ||||||
|       { |       { | ||||||
|         "if": "amenity=parking", |         "if": "amenity=parking", | ||||||
|         "then": { |         "then": { | ||||||
|           "nl": "Auto Parking" |           "nl": "{name:nl}" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         "if": "amenity=motorcycle_parking", |         "if": "amenity=motorcycle_parking", | ||||||
|         "then": { |         "then": { | ||||||
|           "nl": "Motorfiets Parking" |           "nl": "{name}" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|  |  | ||||||
|  | @ -73,10 +73,7 @@ | ||||||
|       }, |       }, | ||||||
|       "tags": [ |       "tags": [ | ||||||
|         "amenity=public_bookcase" |         "amenity=public_bookcase" | ||||||
|       ], |       ] | ||||||
|       "preciseInput": { |  | ||||||
|         "preferredBackground": "photo" |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   ], |   ], | ||||||
|   "tagRenderings": [ |   "tagRenderings": [ | ||||||
|  | @ -142,8 +139,7 @@ | ||||||
|       }, |       }, | ||||||
|       "freeform": { |       "freeform": { | ||||||
|         "key": "capacity", |         "key": "capacity", | ||||||
|         "type": "nat", |         "type": "nat" | ||||||
|         "inline": true |  | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| { | { | ||||||
|   "id": "watermill", |   "id": "watermill", | ||||||
|   "name": { |   "name": { | ||||||
|     "nl": "Watermolens" |     "nl": "watermolens" | ||||||
|   }, |   }, | ||||||
|   "minzoom": 12, |   "minzoom": 12, | ||||||
|   "source": { |   "source": { | ||||||
|  |  | ||||||
|  | @ -1,3 +0,0 @@ | ||||||
| <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> |  | ||||||
| <rect x="1" y="1" width="16" height="16" rx="3" stroke="#007759" stroke-width="2"/> |  | ||||||
| </svg> |  | ||||||
| Before Width: | Height: | Size: 187 B | 
|  | @ -1,4 +0,0 @@ | ||||||
| <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> |  | ||||||
| <rect x="1" y="1" width="16" height="16" rx="3" fill="#007759" stroke="#007759" stroke-width="2"/> |  | ||||||
| <path d="M3.5 8L8 13L14 5" stroke="white" stroke-width="2" stroke-linecap="round"/> |  | ||||||
| </svg> |  | ||||||
| Before Width: | Height: | Size: 286 B | 
|  | @ -1,83 +0,0 @@ | ||||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> |  | ||||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> |  | ||||||
| 
 |  | ||||||
| <svg |  | ||||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" |  | ||||||
|    xmlns:cc="http://creativecommons.org/ns#" |  | ||||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |  | ||||||
|    xmlns:svg="http://www.w3.org/2000/svg" |  | ||||||
|    xmlns="http://www.w3.org/2000/svg" |  | ||||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |  | ||||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |  | ||||||
|    width="100" |  | ||||||
|    height="100" |  | ||||||
|    viewBox="0 0 26.458333 26.458334" |  | ||||||
|    version="1.1" |  | ||||||
|    id="svg8" |  | ||||||
|    inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)" |  | ||||||
|    sodipodi:docname="crosshair-empty.svg"> |  | ||||||
|   <defs |  | ||||||
|      id="defs2" /> |  | ||||||
|   <sodipodi:namedview |  | ||||||
|      id="base" |  | ||||||
|      pagecolor="#ffffff" |  | ||||||
|      bordercolor="#666666" |  | ||||||
|      borderopacity="1.0" |  | ||||||
|      inkscape:pageopacity="0.0" |  | ||||||
|      inkscape:pageshadow="2" |  | ||||||
|      inkscape:zoom="5.6568542" |  | ||||||
|      inkscape:cx="22.669779" |  | ||||||
|      inkscape:cy="52.573519" |  | ||||||
|      inkscape:document-units="px" |  | ||||||
|      inkscape:current-layer="g848" |  | ||||||
|      showgrid="false" |  | ||||||
|      units="px" |  | ||||||
|      showguides="true" |  | ||||||
|      inkscape:guide-bbox="true" |  | ||||||
|      inkscape:window-width="1920" |  | ||||||
|      inkscape:window-height="999" |  | ||||||
|      inkscape:window-x="0" |  | ||||||
|      inkscape:window-y="0" |  | ||||||
|      inkscape:window-maximized="1"> |  | ||||||
|     <sodipodi:guide |  | ||||||
|        position="13.229167,23.859748" |  | ||||||
|        orientation="1,0" |  | ||||||
|        id="guide815" |  | ||||||
|        inkscape:locked="false" /> |  | ||||||
|     <sodipodi:guide |  | ||||||
|        position="14.944824,13.229167" |  | ||||||
|        orientation="0,1" |  | ||||||
|        id="guide817" |  | ||||||
|        inkscape:locked="false" /> |  | ||||||
|   </sodipodi:namedview> |  | ||||||
|   <metadata |  | ||||||
|      id="metadata5"> |  | ||||||
|     <rdf:RDF> |  | ||||||
|       <cc:Work |  | ||||||
|          rdf:about=""> |  | ||||||
|         <dc:format>image/svg+xml</dc:format> |  | ||||||
|         <dc:type |  | ||||||
|            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |  | ||||||
|         <dc:title /> |  | ||||||
|       </cc:Work> |  | ||||||
|     </rdf:RDF> |  | ||||||
|   </metadata> |  | ||||||
|   <g |  | ||||||
|      inkscape:label="Layer 1" |  | ||||||
|      inkscape:groupmode="layer" |  | ||||||
|      id="layer1" |  | ||||||
|      transform="translate(0,-270.54165)"> |  | ||||||
|     <g |  | ||||||
|        id="g848"> |  | ||||||
|       <path |  | ||||||
|          style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#5555ec;fill-opacity:0.98823529;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.26458333;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" |  | ||||||
|          d="m 13.162109,273.57617 c -5.6145729,0 -10.1933596,4.58074 -10.193359,10.19531 -6e-7,5.61458 4.5787861,10.19336 10.193359,10.19336 5.614574,0 10.195313,-4.57878 10.195313,-10.19336 0,-5.61457 -4.580739,-10.19531 -10.195313,-10.19531 z m 0,2.64649 c 4.184659,0 7.548829,3.36417 7.548829,7.54882 0,4.18466 -3.36417,7.54883 -7.548829,7.54883 -4.1846584,0 -7.546875,-3.36417 -7.5468746,-7.54883 -4e-7,-4.18465 3.3622162,-7.54882 7.5468746,-7.54882 z" |  | ||||||
|          id="path815" |  | ||||||
|          inkscape:connector-curvature="0" /> |  | ||||||
|       <path |  | ||||||
|          id="path839" |  | ||||||
|          style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#0055ec;fill-opacity:0.98823529;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.26458333;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" |  | ||||||
|          d="m 13.212891,286.88672 a 1.0487243,1.0487243 0 0 0 -1.033203,1.06445 v 7.94922 a 1.048828,1.048828 0 1 0 2.097656,0 v -7.94922 a 1.0487243,1.0487243 0 0 0 -1.064453,-1.06445 z m 0,-16.36914 a 1.0487243,1.0487243 0 0 0 -1.033203,1.0625 v 7.94922 a 1.048828,1.048828 0 1 0 2.097656,0 v -7.94922 a 1.0487243,1.0487243 0 0 0 -1.064453,-1.0625 z m 4.246093,12.20508 a 1.048825,1.048825 0 1 0 0,2.09765 h 7.949219 a 1.048825,1.048825 0 1 0 0,-2.09765 z m -16.4179684,0 a 1.048825,1.048825 0 1 0 0,2.09765 h 7.9492188 a 1.048825,1.048825 0 1 0 0,-2.09765 z" /> |  | ||||||
|     </g> |  | ||||||
|   </g> |  | ||||||
| </svg> |  | ||||||
| Before Width: | Height: | Size: 5.6 KiB | 
|  | @ -1,106 +0,0 @@ | ||||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> |  | ||||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> |  | ||||||
| 
 |  | ||||||
| <svg |  | ||||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" |  | ||||||
|    xmlns:cc="http://creativecommons.org/ns#" |  | ||||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |  | ||||||
|    xmlns:svg="http://www.w3.org/2000/svg" |  | ||||||
|    xmlns="http://www.w3.org/2000/svg" |  | ||||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |  | ||||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |  | ||||||
|    width="100" |  | ||||||
|    height="100" |  | ||||||
|    viewBox="0 0 26.458333 26.458334" |  | ||||||
|    version="1.1" |  | ||||||
|    id="svg8" |  | ||||||
|    inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)" |  | ||||||
|    sodipodi:docname="crosshair-locked.svg"> |  | ||||||
|   <defs |  | ||||||
|      id="defs2" /> |  | ||||||
|   <sodipodi:namedview |  | ||||||
|      id="base" |  | ||||||
|      pagecolor="#ffffff" |  | ||||||
|      bordercolor="#666666" |  | ||||||
|      borderopacity="1.0" |  | ||||||
|      inkscape:pageopacity="0.0" |  | ||||||
|      inkscape:pageshadow="2" |  | ||||||
|      inkscape:zoom="5.6568542" |  | ||||||
|      inkscape:cx="27.044982" |  | ||||||
|      inkscape:cy="77.667126" |  | ||||||
|      inkscape:document-units="px" |  | ||||||
|      inkscape:current-layer="layer1" |  | ||||||
|      showgrid="false" |  | ||||||
|      units="px" |  | ||||||
|      showguides="true" |  | ||||||
|      inkscape:guide-bbox="true" |  | ||||||
|      inkscape:window-width="1920" |  | ||||||
|      inkscape:window-height="999" |  | ||||||
|      inkscape:window-x="0" |  | ||||||
|      inkscape:window-y="0" |  | ||||||
|      inkscape:window-maximized="1" |  | ||||||
|      inkscape:snap-global="false"> |  | ||||||
|     <sodipodi:guide |  | ||||||
|        position="13.229167,23.859748" |  | ||||||
|        orientation="1,0" |  | ||||||
|        id="guide815" |  | ||||||
|        inkscape:locked="false" /> |  | ||||||
|     <sodipodi:guide |  | ||||||
|        position="14.944824,13.229167" |  | ||||||
|        orientation="0,1" |  | ||||||
|        id="guide817" |  | ||||||
|        inkscape:locked="false" /> |  | ||||||
|   </sodipodi:namedview> |  | ||||||
|   <metadata |  | ||||||
|      id="metadata5"> |  | ||||||
|     <rdf:RDF> |  | ||||||
|       <cc:Work |  | ||||||
|          rdf:about=""> |  | ||||||
|         <dc:format>image/svg+xml</dc:format> |  | ||||||
|         <dc:type |  | ||||||
|            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |  | ||||||
|         <dc:title /> |  | ||||||
|       </cc:Work> |  | ||||||
|     </rdf:RDF> |  | ||||||
|   </metadata> |  | ||||||
|   <g |  | ||||||
|      inkscape:label="Layer 1" |  | ||||||
|      inkscape:groupmode="layer" |  | ||||||
|      id="layer1" |  | ||||||
|      transform="translate(0,-270.54165)"> |  | ||||||
|     <g |  | ||||||
|        id="g827"> |  | ||||||
|       <circle |  | ||||||
|          r="8.8715391" |  | ||||||
|          cy="283.77081" |  | ||||||
|          cx="13.16302" |  | ||||||
|          id="path815" |  | ||||||
|          style="fill:none;fill-opacity:1;stroke:#5555ec;stroke-width:2.64583335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529" /> |  | ||||||
|       <path |  | ||||||
|          inkscape:connector-curvature="0" |  | ||||||
|          id="path817" |  | ||||||
|          d="M 3.2841366,283.77082 H 1.0418969" |  | ||||||
|          style="fill:none;stroke:#5555ec;stroke-width:2.09723878;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" /> |  | ||||||
|       <path |  | ||||||
|          inkscape:connector-curvature="0" |  | ||||||
|          id="path817-3" |  | ||||||
|          d="M 25.405696,283.77082 H 23.286471" |  | ||||||
|          style="fill:none;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" /> |  | ||||||
|       <path |  | ||||||
|          inkscape:connector-curvature="0" |  | ||||||
|          id="path817-3-6" |  | ||||||
|          d="m 13.229167,295.9489 v -2.11763" |  | ||||||
|          style="fill:none;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" /> |  | ||||||
|       <path |  | ||||||
|          inkscape:connector-curvature="0" |  | ||||||
|          id="path817-3-6-7" |  | ||||||
|          d="m 13.229167,275.05759 v -3.44507" |  | ||||||
|          style="fill:none;stroke:#5555ec;stroke-width:2.11666668;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" /> |  | ||||||
|     </g> |  | ||||||
|     <path |  | ||||||
|        style="fill:#5555ec;fill-opacity:0.98823529;stroke-width:0.6151033" |  | ||||||
|        inkscape:connector-curvature="0" |  | ||||||
|        d="m 16.850267,281.91543 h -0.65616 v -1.85094 c 0,0 0,-3.08489 -3.066169,-3.08489 -3.066169,0 -3.066169,3.08489 -3.066169,3.08489 v 1.85094 H 9.4056091 a 1.1835412,1.1907685 0 0 0 -1.1835412,1.19077 v 5.02838 a 1.1835412,1.1907685 0 0 0 1.1835412,1.1846 h 7.4446579 a 1.1835412,1.1907685 0 0 0 1.183541,-1.19078 v -5.0222 a 1.1835412,1.1907685 0 0 0 -1.183541,-1.19077 z m -3.722329,4.93583 a 1.2264675,1.233957 0 1 1 1.226468,-1.23395 1.2264675,1.233957 0 0 1 -1.226468,1.23395 z m 1.839702,-4.93583 h -3.679403 v -1.54245 c 0,-0.92546 0,-2.15942 1.839701,-2.15942 1.839702,0 1.839702,1.23396 1.839702,2.15942 z" |  | ||||||
|        id="path822" /> |  | ||||||
|   </g> |  | ||||||
| </svg> |  | ||||||
| Before Width: | Height: | Size: 4.3 KiB | 
|  | @ -1,115 +0,0 @@ | ||||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> |  | ||||||
| <svg |  | ||||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" |  | ||||||
|    xmlns:cc="http://creativecommons.org/ns#" |  | ||||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |  | ||||||
|    xmlns:svg="http://www.w3.org/2000/svg" |  | ||||||
|    xmlns="http://www.w3.org/2000/svg" |  | ||||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |  | ||||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |  | ||||||
|    version="1.0" |  | ||||||
|    width="859.53607pt" |  | ||||||
|    height="858.4754pt" |  | ||||||
|    viewBox="0 0 859.53607 858.4754" |  | ||||||
|    preserveAspectRatio="xMidYMid meet" |  | ||||||
|    id="svg14" |  | ||||||
|    sodipodi:docname="length-crosshair.svg" |  | ||||||
|    inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"> |  | ||||||
|   <defs |  | ||||||
|      id="defs18" /> |  | ||||||
|   <sodipodi:namedview |  | ||||||
|      pagecolor="#ffffff" |  | ||||||
|      bordercolor="#666666" |  | ||||||
|      borderopacity="1" |  | ||||||
|      objecttolerance="10" |  | ||||||
|      gridtolerance="10" |  | ||||||
|      guidetolerance="10" |  | ||||||
|      inkscape:pageopacity="0" |  | ||||||
|      inkscape:pageshadow="2" |  | ||||||
|      inkscape:window-width="1920" |  | ||||||
|      inkscape:window-height="999" |  | ||||||
|      id="namedview16" |  | ||||||
|      showgrid="false" |  | ||||||
|      showguides="true" |  | ||||||
|      inkscape:guide-bbox="true" |  | ||||||
|      inkscape:zoom="0.5" |  | ||||||
|      inkscape:cx="307.56567" |  | ||||||
|      inkscape:cy="-35.669379" |  | ||||||
|      inkscape:window-x="0" |  | ||||||
|      inkscape:window-y="0" |  | ||||||
|      inkscape:window-maximized="1" |  | ||||||
|      inkscape:current-layer="svg14" |  | ||||||
|      inkscape:snap-smooth-nodes="true" /> |  | ||||||
|   <metadata |  | ||||||
|      id="metadata2"> |  | ||||||
| Created by potrace 1.15, written by Peter Selinger 2001-2017 |  | ||||||
| <rdf:RDF> |  | ||||||
|   <cc:Work |  | ||||||
|      rdf:about=""> |  | ||||||
|     <dc:format>image/svg+xml</dc:format> |  | ||||||
|     <dc:type |  | ||||||
|        rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |  | ||||||
|     <dc:title /> |  | ||||||
|   </cc:Work> |  | ||||||
| </rdf:RDF> |  | ||||||
| </metadata> |  | ||||||
|   <path |  | ||||||
|      style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:2.99999994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:71.99999853,71.99999853;stroke-dashoffset:0;stroke-opacity:1" |  | ||||||
|      id="path816" |  | ||||||
|      transform="rotate(-89.47199)" |  | ||||||
|      sodipodi:type="arc" |  | ||||||
|      sodipodi:cx="-425.24921" |  | ||||||
|      sodipodi:cy="433.71375" |  | ||||||
|      sodipodi:rx="428.34982" |  | ||||||
|      sodipodi:ry="427.81949" |  | ||||||
|      sodipodi:start="0" |  | ||||||
|      sodipodi:end="4.7117019" |  | ||||||
|      sodipodi:open="true" |  | ||||||
|      d="M 3.1006165,433.71375 A 428.34982,427.81949 0 0 1 -425.1511,861.53322 428.34982,427.81949 0 0 1 -853.59898,433.90971 428.34982,427.81949 0 0 1 -425.54352,5.8943576" /> |  | ||||||
|   <path |  | ||||||
|      style="fill:none;stroke:#000000;stroke-width:4.49999991;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" |  | ||||||
|      d="m 429.76804,430.08754 0,-429.19968" |  | ||||||
|      id="path820" |  | ||||||
|      inkscape:connector-curvature="0" /> |  | ||||||
|   <path |  | ||||||
|      style="fill:none;stroke:#000000;stroke-width:1.49999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:35.99999926,35.99999926;stroke-dashoffset:0" |  | ||||||
|      d="m 857.58749,429.23771 -855.6389371,0 v 0" |  | ||||||
|      id="path822" |  | ||||||
|      inkscape:connector-curvature="0" /> |  | ||||||
|   <path |  | ||||||
|      inkscape:connector-curvature="0" |  | ||||||
|      id="path814" |  | ||||||
|      d="M 429.76804,857.30628 V 428.78674" |  | ||||||
|      style="fill:none;stroke:#000000;stroke-width:1.49999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:35.99999926,35.99999926;stroke-dashoffset:0" /> |  | ||||||
|   <path |  | ||||||
|      inkscape:connector-curvature="0" |  | ||||||
|      id="path826" |  | ||||||
|      d="M 857.32232,1.0332137 H 1.6833879 v 0" |  | ||||||
|      style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:17.99999963, 17.99999963;stroke-dashoffset:0;stroke-opacity:1" /> |  | ||||||
|   <path |  | ||||||
|      inkscape:connector-curvature="0" |  | ||||||
|      id="path828" |  | ||||||
|      d="M 857.58749,858.2377 H 1.9485529 v 0" |  | ||||||
|      style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:8.99999982, 8.99999982;stroke-dashoffset:0;stroke-opacity:1" /> |  | ||||||
|   <path |  | ||||||
|      cx="-429.2377" |  | ||||||
|      cy="429.76804" |  | ||||||
|      rx="428.34982" |  | ||||||
|      ry="427.81949" |  | ||||||
|      transform="rotate(-90)" |  | ||||||
|      id="path825" |  | ||||||
|      style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:11.99999975;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> |  | ||||||
|   <path |  | ||||||
|      d="M -5.3639221,-424.71887 A 428.34982,427.81949 0 0 1 -429.83855,3.0831087 428.34982,427.81949 0 0 1 -861.99345,-416.97839" |  | ||||||
|      sodipodi:open="true" |  | ||||||
|      sodipodi:end="3.1234988" |  | ||||||
|      sodipodi:start="0" |  | ||||||
|      sodipodi:ry="427.81949" |  | ||||||
|      sodipodi:rx="428.34982" |  | ||||||
|      sodipodi:cy="-424.71887" |  | ||||||
|      sodipodi:cx="-433.71375" |  | ||||||
|      sodipodi:type="arc" |  | ||||||
|      transform="rotate(-179.47199)" |  | ||||||
|      id="path827" |  | ||||||
|      style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:4.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> |  | ||||||
| </svg> |  | ||||||
| Before Width: | Height: | Size: 4.7 KiB | 
|  | @ -614,635 +614,5 @@ | ||||||
|     "path": "filter.svg", |     "path": "filter.svg", | ||||||
|     "license": "CC0", |     "license": "CC0", | ||||||
|     "sources": [] |     "sources": [] | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Hannah Declerck" |  | ||||||
|     ], |  | ||||||
|     "path": "checkbox-empty.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Hannah Declerck" |  | ||||||
|     ], |  | ||||||
|     "path": "checkbox-filled.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Hannah Declerck" |  | ||||||
|     ], |  | ||||||
|     "path": "arrow-left-thin.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "path": "direction_masked.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "path": "direction_outline.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "path": "direction_stroke.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "path": "SocialImageForeground.svg", |  | ||||||
|     "license": "CC-BY-SA", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://mapcomplete.osm.be" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "path": "add.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "path": "addSmall.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "ampersand.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "path": "arrow-left-smooth.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "path": "arrow-right-smooth.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "path": "back.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Github" |  | ||||||
|     ], |  | ||||||
|     "path": "bug.svg", |  | ||||||
|     "license": "MIT", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://commons.wikimedia.org/wiki/File:Octicons-bug.svg", |  | ||||||
|       " https://github.com/primer/octicons" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "path": "camera-plus.svg", |  | ||||||
|     "license": "CC-BY-SA 3.0", |  | ||||||
|     "authors": [ |  | ||||||
|       "Dave Gandy", |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "sources": [ |  | ||||||
|       "https://fontawesome.com/", |  | ||||||
|       "https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "path": "checkmark.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "path": "circle.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet" |  | ||||||
|     ], |  | ||||||
|     "path": "clock.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "close.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "compass.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "cross_bottom_right.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "crosshair-blue-center.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "crosshair-blue.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "crosshair.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "crosshair-empty.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "crosshair-locked.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Dave Gandy" |  | ||||||
|     ], |  | ||||||
|     "path": "delete_icon.svg", |  | ||||||
|     "license": "CC-BY-SA", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://commons.wikimedia.org/wiki/File:Trash_font_awesome.svg\rT" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "direction.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "direction_gradient.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "down.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "envelope.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "The Tango Desktop Project" |  | ||||||
|     ], |  | ||||||
|     "path": "floppy.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://commons.wikimedia.org/wiki/File:Media-floppy.svg", |  | ||||||
|       "http://tango.freedesktop.org/Tango_Desktop_Project" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "gear.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "help.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Timothy Miller" |  | ||||||
|     ], |  | ||||||
|     "path": "home.svg", |  | ||||||
|     "license": "CC-BY-SA 3.0", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://commons.wikimedia.org/wiki/File:Home-icon.svg" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Timothy Miller" |  | ||||||
|     ], |  | ||||||
|     "path": "home_white_bg.svg", |  | ||||||
|     "license": "CC-BY-SA 3.0", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://commons.wikimedia.org/wiki/File:Home-icon.svg" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "JOSM Team" |  | ||||||
|     ], |  | ||||||
|     "path": "josm_logo.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://wiki.openstreetmap.org/wiki/File:JOSM_Logotype_2019.svg", |  | ||||||
|       " https://josm.openstreetmap.de/" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "layers.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "layersAdd.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "path": "Ornament-Horiz-0.svg", |  | ||||||
|     "license": "CC-BY", |  | ||||||
|     "authors": [ |  | ||||||
|       "Nightwolfdezines" |  | ||||||
|     ], |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "path": "Ornament-Horiz-1.svg", |  | ||||||
|     "license": "CC-BY", |  | ||||||
|     "authors": [ |  | ||||||
|       "Nightwolfdezines" |  | ||||||
|     ], |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "path": "Ornament-Horiz-2.svg", |  | ||||||
|     "license": "CC-BY", |  | ||||||
|     "authors": [ |  | ||||||
|       "Nightwolfdezines" |  | ||||||
|     ], |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "path": "Ornament-Horiz-3.svg", |  | ||||||
|     "license": "CC-BY", |  | ||||||
|     "authors": [ |  | ||||||
|       "Nightwolfdezines" |  | ||||||
|     ], |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "path": "Ornament-Horiz-4.svg", |  | ||||||
|     "license": "CC-BY", |  | ||||||
|     "authors": [ |  | ||||||
|       "Nightwolfdezines" |  | ||||||
|     ], |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "path": "Ornament-Horiz-5.svg", |  | ||||||
|     "license": "CC-BY", |  | ||||||
|     "authors": [ |  | ||||||
|       "Nightwolfdezines" |  | ||||||
|     ], |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "path": "Ornament-Horiz-6.svg", |  | ||||||
|     "license": "CC-BY", |  | ||||||
|     "authors": [ |  | ||||||
|       "Nightwolfdezines" |  | ||||||
|     ], |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "star.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "star_outline.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "star_half.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "star_outline_half.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "logo.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "logout.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Pieter Vander Vennet", |  | ||||||
|       " OSM" |  | ||||||
|     ], |  | ||||||
|     "path": "mapcomplete_logo.svg", |  | ||||||
|     "license": "Logo; CC-BY-SA", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://mapcomplete.osm.be" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Mapillary" |  | ||||||
|     ], |  | ||||||
|     "path": "mapillary.svg", |  | ||||||
|     "license": "Logo; All rights reserved", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://mapillary.com/" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "min.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "no_checkmark.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "or.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "osm-copyright.svg", |  | ||||||
|     "license": "logo; all rights reserved", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.OpenStreetMap.org" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "OpenStreetMap U.S. Chapter" |  | ||||||
|     ], |  | ||||||
|     "path": "osm-logo-us.svg", |  | ||||||
|     "license": "Logo", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.openstreetmap.us/" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "osm-logo.svg", |  | ||||||
|     "license": "logo; all rights reserved", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.OpenStreetMap.org" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "GitHub Octicons" |  | ||||||
|     ], |  | ||||||
|     "path": "pencil.svg", |  | ||||||
|     "license": "MIT", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://github.com/primer/octicons", |  | ||||||
|       " https://commons.wikimedia.org/wiki/File:Octicons-pencil.svg" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "@ tyskrat" |  | ||||||
|     ], |  | ||||||
|     "path": "phone.svg", |  | ||||||
|     "license": "CC-BY 3.0", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.onlinewebfonts.com/icon/1059" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "pin.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "plus.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "@fatih" |  | ||||||
|     ], |  | ||||||
|     "path": "pop-out.svg", |  | ||||||
|     "license": "CC-BY 3.0", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.onlinewebfonts.com/icon/2151" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "reload.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "ring.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "OOjs UI Team and other contributors" |  | ||||||
|     ], |  | ||||||
|     "path": "search.svg", |  | ||||||
|     "license": "MIT", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://commons.wikimedia.org/wiki/File:OOjs_UI_indicator_search-rtl.svg", |  | ||||||
|       "https://phabricator.wikimedia.org/diffusion/GOJU/browse/master/AUTHORS.txt" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "send_email.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "share.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "square.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "@felpgrc" |  | ||||||
|     ], |  | ||||||
|     "path": "statistics.svg", |  | ||||||
|     "license": "CC-BY 3.0", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.onlinewebfonts.com/icon/197818" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "MGalloway (WMF)" |  | ||||||
|     ], |  | ||||||
|     "path": "translate.svg", |  | ||||||
|     "license": "CC-BY-SA 3.0", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://commons.wikimedia.org/wiki/File:OOjs_UI_icon_language-ltr.svg" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [], |  | ||||||
|     "path": "up.svg", |  | ||||||
|     "license": "CC0; trivial", |  | ||||||
|     "sources": [] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Wikidata" |  | ||||||
|     ], |  | ||||||
|     "path": "wikidata.svg", |  | ||||||
|     "license": "Logo; All rights reserved", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.wikidata.org" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Wikimedia" |  | ||||||
|     ], |  | ||||||
|     "path": "wikimedia-commons-white.svg", |  | ||||||
|     "license": "Logo; All rights reserved", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://commons.wikimedia.org" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Wikipedia" |  | ||||||
|     ], |  | ||||||
|     "path": "wikipedia.svg", |  | ||||||
|     "license": "Logo; All rights reserved", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.wikipedia.org/" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "Mapillary" |  | ||||||
|     ], |  | ||||||
|     "path": "mapillary_black.svg", |  | ||||||
|     "license": "Logo; All rights reserved", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://www.mapillary.com/" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "authors": [ |  | ||||||
|       "The Tango! Desktop Project" |  | ||||||
|     ], |  | ||||||
|     "path": "floppy.svg", |  | ||||||
|     "license": "CC0", |  | ||||||
|     "sources": [ |  | ||||||
|       "https://commons.wikimedia.org/wiki/File:Media-floppy.svg" |  | ||||||
|     ] |  | ||||||
|   } |   } | ||||||
| ] | ] | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| { | { | ||||||
|   "wikipedialink": { |   "wikipedialink": { | ||||||
|     "render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank'><img src='./assets/svg/wikipedia.svg' alt='WP'/></a>", |     "render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank'><img src='./assets/svg/wikipedia.svg' alt='WP'/></a>", | ||||||
|     "condition": { |     "condition": "wikipedia~*", | ||||||
|       "or": [ |     "mappings": [ | ||||||
|         "wikipedia~*", |       { | ||||||
|  |         "if": { | ||||||
|  |           "and": [ | ||||||
|  |             "wikipedia=", | ||||||
|             "wikidata~*" |             "wikidata~*" | ||||||
|           ] |           ] | ||||||
|         }, |         }, | ||||||
|     "mappings": [ |  | ||||||
|       { |  | ||||||
|         "if": "wikipedia=", |  | ||||||
|         "then": "<a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>" |         "then": "<a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>" | ||||||
|       } |       } | ||||||
|     ] |     ] | ||||||
|  | @ -59,12 +59,8 @@ | ||||||
|     "render": "<a href='https://openstreetmap.org/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>", |     "render": "<a href='https://openstreetmap.org/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>", | ||||||
|     "mappings": [ |     "mappings": [ | ||||||
|       { |       { | ||||||
|         "if": "id~.*/-.*", |         "if": "id~=-", | ||||||
|         "then": "" |         "then": "<span class='alert'>Uploading...</alert>" | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         "if": "_backend~*", |  | ||||||
|         "then": "<a href='{_backend}/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>" |  | ||||||
|       } |       } | ||||||
|     ], |     ], | ||||||
|     "condition": "id~(node|way|relation)/[0-9]*" |     "condition": "id~(node|way|relation)/[0-9]*" | ||||||
|  |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								assets/themes/.DS_Store
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/themes/.DS_Store
									
										
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -736,7 +736,7 @@ | ||||||
|         "_contained_climbing_route_ids=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p.id)", |         "_contained_climbing_route_ids=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p.id)", | ||||||
|         "_difficulty_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:grade:french'])", |         "_difficulty_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:grade:french'])", | ||||||
|         "_length_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:length'])", |         "_length_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:length'])", | ||||||
|         "_contained_climbing_routes_count=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').length" |         "_contained_climbing_routes_count=JSON.parse(_contained_climbing_routes).length" | ||||||
|       ] |       ] | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|  | @ -1412,8 +1412,8 @@ | ||||||
|       "_embedding_feature_properties=feat.overlapWith('climbing').map(f => f.feat.properties).filter(p => p !== undefined).map(p => {return{access: p.access, id: p.id, name: p.name, climbing: p.climbing, 'access:description': p['access:description']}})", |       "_embedding_feature_properties=feat.overlapWith('climbing').map(f => f.feat.properties).filter(p => p !== undefined).map(p => {return{access: p.access, id: p.id, name: p.name, climbing: p.climbing, 'access:description': p['access:description']}})", | ||||||
|       "_embedding_features_with_access=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.access !== undefined)[0]", |       "_embedding_features_with_access=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.access !== undefined)[0]", | ||||||
|       "_embedding_feature_with_rock=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.rock !== undefined)[0] ?? '{}'", |       "_embedding_feature_with_rock=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.rock !== undefined)[0] ?? '{}'", | ||||||
|       "_embedding_features_with_rock:rock=JSON.parse(feat.properties._embedding_feature_with_rock ?? '{}')?.rock", |       "_embedding_features_with_rock:rock=JSON.parse(_embedding_feature_with_rock)?.rock", | ||||||
|       "_embedding_features_with_rock:id=JSON.parse(feat.properties._embedding_feature_with_rock ?? '{}')?.id", |       "_embedding_features_with_rock:id=JSON.parse(_embedding_feature_with_rock)?.id", | ||||||
|       "_embedding_feature:access=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').access", |       "_embedding_feature:access=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').access", | ||||||
|       "_embedding_feature:access:description=JSON.parse(feat.properties._embedding_features_with_access ?? '{}')['access:description']", |       "_embedding_feature:access:description=JSON.parse(feat.properties._embedding_features_with_access ?? '{}')['access:description']", | ||||||
|       "_embedding_feature:id=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').id" |       "_embedding_feature:id=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').id" | ||||||
|  |  | ||||||
|  | @ -24,58 +24,36 @@ | ||||||
|   "startZoom": 15, |   "startZoom": 15, | ||||||
|   "widenFactor": 0.05, |   "widenFactor": 0.05, | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "defaultBackgroundId": "CartoDB.Positron", |  | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|       "#": "Nature reserve with geometry, z>=13", |       "builtin": [ | ||||||
|       "builtin": "nature_reserve", |         "nature_reserve" | ||||||
|  |       ], | ||||||
|       "override": { |       "override": { | ||||||
|         "source": { |         "source": { | ||||||
|           "osmTags": { |           "osmTags": { | ||||||
|             "+and": [ |             "+and": [ | ||||||
|               "operator~.*[nN]atuurpunt.*" |               "operator~.*[nN]atuurpunt.*" | ||||||
|             ] |             ] | ||||||
|  |           } | ||||||
|         }, |         }, | ||||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", |         "minzoom": "10", | ||||||
|           "geoJsonZoomLevel": 12, |  | ||||||
|           "isOsmCache": true |  | ||||||
|         }, |  | ||||||
|         "minzoom": "13", |  | ||||||
|         "icon": { |         "icon": { | ||||||
|           "render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg" |           "render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg" | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "#": "Nature reserve overview from cache, points only, z < 13", |       "builtin": [ | ||||||
|       "builtin": "nature_reserve", |         "visitor_information_centre" | ||||||
|  |       ], | ||||||
|       "override": { |       "override": { | ||||||
|         "source": { |         "source": { | ||||||
|           "osmTags": { |           "osmTags": { | ||||||
|             "+and": [ |             "+and": [ | ||||||
|               "operator~.*[nN]atuurpunt.*" |               "operator~.*[nN]atuurpunt.*" | ||||||
|             ] |             ] | ||||||
|           }, |  | ||||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_nature_reserve_points.geojson" |  | ||||||
|         }, |  | ||||||
|         "minzoom": "0", |  | ||||||
|         "icon": { |  | ||||||
|           "render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg" |  | ||||||
|           } |           } | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       "builtin": "visitor_information_centre", |  | ||||||
|       "override": { |  | ||||||
|         "source": { |  | ||||||
|           "osmTags": { |  | ||||||
|             "+and": [ |  | ||||||
|               "operator~.*[nN]atuurpunt.*" |  | ||||||
|             ] |  | ||||||
|           }, |  | ||||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", |  | ||||||
|           "geoJsonZoomLevel": 12, |  | ||||||
|           "isOsmCache": true |  | ||||||
|         }, |         }, | ||||||
|         "minzoom": "10", |         "minzoom": "10", | ||||||
|         "icon": { |         "icon": { | ||||||
|  | @ -84,17 +62,16 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "builtin": "trail", |       "builtin": [ | ||||||
|  |         "trail" | ||||||
|  |       ], | ||||||
|       "override": { |       "override": { | ||||||
|         "source": { |         "source": { | ||||||
|           "osmTags": { |           "osmTags": { | ||||||
|             "+and": [ |             "+and": [ | ||||||
|               "operator~.*[nN]atuurpunt.*" |               "operator~.*[nN]atuurpunt.*" | ||||||
|             ] |             ] | ||||||
|           }, |           } | ||||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", |  | ||||||
|           "geoJsonZoomLevel": 12, |  | ||||||
|           "isOsmCache": true |  | ||||||
|         }, |         }, | ||||||
|         "minzoom": "13", |         "minzoom": "13", | ||||||
|         "icon": { |         "icon": { | ||||||
|  | @ -113,14 +90,11 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "builtin": "toilet", |       "builtin": [ | ||||||
|  |         "toilet" | ||||||
|  |       ], | ||||||
|       "override": { |       "override": { | ||||||
|         "minzoom": "15", |         "minzoom": "15", | ||||||
|         "source": { |  | ||||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", |  | ||||||
|           "geoJsonZoomLevel": 12, |  | ||||||
|           "isOsmCache": true |  | ||||||
|         }, |  | ||||||
|         "icon": { |         "icon": { | ||||||
|           "render": "circle:#FE6F32;./assets/themes/natuurpunt/toilets.svg", |           "render": "circle:#FE6F32;./assets/themes/natuurpunt/toilets.svg", | ||||||
|           "mappings": [ |           "mappings": [ | ||||||
|  | @ -137,61 +111,42 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "builtin": "birdhide", |       "builtin": [ | ||||||
|  |         "birdhide" | ||||||
|  |       ], | ||||||
|       "override": { |       "override": { | ||||||
|         "minzoom": "15", |         "minzoom": "15", | ||||||
|         "source": { |  | ||||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", |  | ||||||
|           "geoJsonZoomLevel": 12, |  | ||||||
|           "isOsmCache": true |  | ||||||
|         }, |  | ||||||
|         "icon": { |         "icon": { | ||||||
|           "render": "circle:#FE6F32;./assets/themes/natuurpunt/birdhide.svg", |           "render": "circle:#FE6F32;./assets/themes/natuurpunt/birdhide.svg" | ||||||
|           "mappings": [ |  | ||||||
|             { |  | ||||||
|               "if": { |  | ||||||
|                 "or": [ |  | ||||||
|                   "building=yes", |  | ||||||
|                   "shelter=yes", |  | ||||||
|                   "amenity=shelter" |  | ||||||
|                 ] |  | ||||||
|               }, |  | ||||||
|               "then": "circle:#FE6F32;./assets/themes/natuurpunt/birdshelter.svg" |  | ||||||
|             } |  | ||||||
|           ] |  | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "builtin": "picnic_table", |       "builtin": [ | ||||||
|  |         "picnic_table" | ||||||
|  |       ], | ||||||
|       "override": { |       "override": { | ||||||
|         "minzoom": "16", |         "minzoom": "16", | ||||||
|         "source": { |  | ||||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", |  | ||||||
|           "geoJsonZoomLevel": 12, |  | ||||||
|           "isOsmCache": true |  | ||||||
|         }, |  | ||||||
|         "icon": { |         "icon": { | ||||||
|           "render": "circle:#FE6F32;./assets/themes/natuurpunt/picnic_table.svg" |           "render": "circle:#FE6F32;./assets/themes/natuurpunt/picnic_table.svg" | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "builtin": "drinking_water", |       "builtin": [ | ||||||
|  |         "drinking_water" | ||||||
|  |       ], | ||||||
|       "override": { |       "override": { | ||||||
|         "minzoom": "16", |         "minzoom": "16", | ||||||
|         "source": { |  | ||||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", |  | ||||||
|           "geoJsonZoomLevel": 12, |  | ||||||
|           "isOsmCache": true |  | ||||||
|         }, |  | ||||||
|         "icon": { |         "icon": { | ||||||
|           "render": "circle:#FE6F32;./assets/themes/natuurpunt/drips.svg" |           "render": "circle:#FE6F32;./assets/themes/natuurpunt/drips.svg" | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "builtin": "parking", |       "builtin": [ | ||||||
|  |         "parking" | ||||||
|  |       ], | ||||||
|       "override": { |       "override": { | ||||||
|         "minzoom": "16", |         "minzoom": "16", | ||||||
|         "icon": { |         "icon": { | ||||||
|  | @ -218,42 +173,33 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "builtin": "information_board", |       "builtin": [ | ||||||
|  |         "information_board" | ||||||
|  |       ], | ||||||
|       "override": { |       "override": { | ||||||
|         "minzoom": "16", |         "minzoom": "16", | ||||||
|         "source": { |  | ||||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", |  | ||||||
|           "geoJsonZoomLevel": 12, |  | ||||||
|           "isOsmCache": true |  | ||||||
|         }, |  | ||||||
|         "icon": { |         "icon": { | ||||||
|           "render": "circle:#FE6F32;./assets/themes/natuurpunt/information_board.svg" |           "render": "circle:#FE6F32;./assets/themes/natuurpunt/information_board.svg" | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "builtin": "bench", |       "builtin": [ | ||||||
|  |         "bench" | ||||||
|  |       ], | ||||||
|       "override": { |       "override": { | ||||||
|         "minzoom": "18", |         "minzoom": "18", | ||||||
|         "source": { |  | ||||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", |  | ||||||
|           "geoJsonZoomLevel": 12, |  | ||||||
|           "isOsmCache": true |  | ||||||
|         }, |  | ||||||
|         "icon": { |         "icon": { | ||||||
|           "render": "circle:#FE6F32;./assets/themes/natuurpunt/bench.svg" |           "render": "circle:#FE6F32;./assets/themes/natuurpunt/bench.svg" | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "builtin": "watermill", |       "builtin": [ | ||||||
|  |         "watermill" | ||||||
|  |       ], | ||||||
|       "override": { |       "override": { | ||||||
|         "minzoom": "18", |         "minzoom": "18", | ||||||
|         "source": { |  | ||||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", |  | ||||||
|           "geoJsonZoomLevel": 12, |  | ||||||
|           "isOsmCache": true |  | ||||||
|         }, |  | ||||||
|         "icon": { |         "icon": { | ||||||
|           "render": "circle:#FE6F32;./assets/themes/natuurpunt/watermill.svg" |           "render": "circle:#FE6F32;./assets/themes/natuurpunt/watermill.svg" | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -62,8 +62,7 @@ | ||||||
|             "en": "What is the power output of this wind turbine? (e.g. 2.3 MW)" |             "en": "What is the power output of this wind turbine? (e.g. 2.3 MW)" | ||||||
|           }, |           }, | ||||||
|           "freeform": { |           "freeform": { | ||||||
|             "key": "generator:output:electricity", |             "key": "generator:output:electricity" | ||||||
|             "type": "pfloat" |  | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|  | @ -86,7 +85,7 @@ | ||||||
|           }, |           }, | ||||||
|           "freeform": { |           "freeform": { | ||||||
|             "key": "height", |             "key": "height", | ||||||
|             "type": "pfloat" |             "type": "float" | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|  | @ -180,24 +179,6 @@ | ||||||
|         } |         } | ||||||
|       ], |       ], | ||||||
|       "eraseInvalidValues": true |       "eraseInvalidValues": true | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       "appliesToKey": [ |  | ||||||
|         "height", |  | ||||||
|         "rotor:diameter" |  | ||||||
|       ], |  | ||||||
|       "applicableUnits": [ |  | ||||||
|         { |  | ||||||
|           "canonicalDenomination": "m", |  | ||||||
|           "alternativeDenomination": [ |  | ||||||
|             "meter" |  | ||||||
|           ], |  | ||||||
|           "human": { |  | ||||||
|             "en": " meter", |  | ||||||
|             "nl": " meter" |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       ] |  | ||||||
|     } |     } | ||||||
|   ], |   ], | ||||||
|   "defaultBackgroundId": "CartoDB.Voyager" |   "defaultBackgroundId": "CartoDB.Voyager" | ||||||
|  |  | ||||||
|  | @ -105,31 +105,11 @@ | ||||||
|     { |     { | ||||||
|       "builtin": "slow_roads", |       "builtin": "slow_roads", | ||||||
|       "override": { |       "override": { | ||||||
|         "+tagRenderings": [ |  | ||||||
|           { |  | ||||||
|             "question": "Is dit een publiek toegankelijk pad?", |  | ||||||
|             "mappings": [ |  | ||||||
|               { |  | ||||||
|                 "if": "access=private", |  | ||||||
|                 "then": "Dit is een privaat pad" |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 "if": "access=no", |  | ||||||
|                 "then": "Dit is een privaat pad", |  | ||||||
|                 "hideInAnswer": true |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 "if": "access=permissive", |  | ||||||
|                 "then": "Dit pad is duidelijk in private eigendom, maar er hangen geen verbodsborden dus mag men erover" |  | ||||||
|               } |  | ||||||
|             ] |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         "calculatedTags": [ |         "calculatedTags": [ | ||||||
|           "_part_of_walking_routes=Array.from(new Set(feat.memberships().map(r => \"<a href='#relation/\"+r.relation.id+\"'>\" + r.relation.tags.name + \"</a>\"))).join(', ')", |           "_part_of_walking_routes=Array.from(new Set(feat.memberships().map(r => \"<a href='#relation/\"+r.relation.id+\"'>\" + r.relation.tags.name + \"</a>\"))).join(', ')", | ||||||
|           "_is_shadowed=feat.overlapWith('shadow').length > 0 ? 'yes': ''" |           "_is_shadowed=feat.overlapWith('shadow').length > 0 ? 'yes': ''" | ||||||
|         ], |         ], | ||||||
|         "minzoom": 18, |         "minzoom": 9, | ||||||
|         "source": { |         "source": { | ||||||
|           "geoJsonLocal": "http://127.0.0.1:8080/speelplekken_{layer}_{z}_{x}_{y}.geojson", |           "geoJsonLocal": "http://127.0.0.1:8080/speelplekken_{layer}_{z}_{x}_{y}.geojson", | ||||||
|           "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson", |           "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson", | ||||||
|  |  | ||||||
|  | @ -64,13 +64,7 @@ | ||||||
|       }, |       }, | ||||||
|       "tagRenderings": [ |       "tagRenderings": [ | ||||||
|         { |         { | ||||||
|           "render": "Deze straat is <b>{width:carriageway}m</b> breed", |           "render": "Deze straat is <b>{width:carriageway}m</b> breed" | ||||||
|           "question": "Hoe breed is deze straat?", |  | ||||||
|           "freeform": { |  | ||||||
|             "key": "width:carriageway", |  | ||||||
|             "type": "length", |  | ||||||
|             "helperArgs": [21, "map"] |  | ||||||
|           } |  | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           "render": "Deze straat heeft <span class='alert'>{_width:difference}m</span> te weinig:", |           "render": "Deze straat heeft <span class='alert'>{_width:difference}m</span> te weinig:", | ||||||
|  |  | ||||||
|  | @ -82,10 +82,6 @@ html, body { | ||||||
|     box-sizing: initial !important; |     box-sizing: initial !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .leaflet-control-attribution { |  | ||||||
|     display: block ruby; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| svg, img { | svg, img { | ||||||
|     box-sizing: content-box; |     box-sizing: content-box; | ||||||
|     width: 100%; |     width: 100%; | ||||||
|  | @ -105,10 +101,6 @@ a { | ||||||
|     width: min-content; |     width: min-content; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .w-16-imp { |  | ||||||
|     width: 4rem !important; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .space-between{ | .space-between{ | ||||||
|     justify-content: space-between; |     justify-content: space-between; | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										3
									
								
								index.ts
									
										
									
									
									
								
							|  | @ -19,13 +19,10 @@ import DirectionInput from "./UI/Input/DirectionInput"; | ||||||
| import SpecialVisualizations from "./UI/SpecialVisualizations"; | import SpecialVisualizations from "./UI/SpecialVisualizations"; | ||||||
| import ShowDataLayer from "./UI/ShowDataLayer"; | import ShowDataLayer from "./UI/ShowDataLayer"; | ||||||
| import * as L from "leaflet"; | import * as L from "leaflet"; | ||||||
| import ValidatedTextField from "./UI/Input/ValidatedTextField"; |  | ||||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; |  | ||||||
| 
 | 
 | ||||||
| // Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
 | // Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
 | ||||||
| SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); | SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); | ||||||
| DirectionInput.constructMinimap = options =>  new Minimap(options) | DirectionInput.constructMinimap = options =>  new Minimap(options) | ||||||
| ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref)  |  | ||||||
| SpecialVisualizations.constructMiniMap = options => new Minimap(options) | SpecialVisualizations.constructMiniMap = options => new Minimap(options) | ||||||
| SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>, | SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>, | ||||||
|                                                  leafletMap: UIEventSource<L.Map>, |                                                  leafletMap: UIEventSource<L.Map>, | ||||||
|  |  | ||||||
|  | @ -149,10 +149,6 @@ | ||||||
|       "zoomInToSeeThisLayer": "Zoom in to see this layer", |       "zoomInToSeeThisLayer": "Zoom in to see this layer", | ||||||
|       "title": "Select layers" |       "title": "Select layers" | ||||||
|     }, |     }, | ||||||
|     "download": { |  | ||||||
|       "downloadGeojson": "Download visible data as geojson", |  | ||||||
|       "licenseInfo": "<h3>Copyright notice</h3>The provided is available under ODbL. Reusing this data is free for any purpose, but <ul><li>the attribution <b>© OpenStreetMap contributors</b></li><li>Any change to this data must be republished under the same license</li></ul>. Please see the full <a href='https://www.openstreetmap.org/copyright' target='_blank'>copyright notice</a> for details" |  | ||||||
|     }, |  | ||||||
|     "weekdays": { |     "weekdays": { | ||||||
|       "abbreviations": { |       "abbreviations": { | ||||||
|         "monday": "Mon", |         "monday": "Mon", | ||||||
|  |  | ||||||
|  | @ -1185,6 +1185,9 @@ | ||||||
|                 }, |                 }, | ||||||
|                 "1": { |                 "1": { | ||||||
|                     "then": "{name}" |                     "then": "{name}" | ||||||
|  |                 }, | ||||||
|  |                 "2": { | ||||||
|  |                     "then": "Fietsenstalling" | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|  | @ -487,11 +487,6 @@ | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }, |  | ||||||
|         "presets": { |  | ||||||
|             "0": { |  | ||||||
|                 "title": "Обслуживание велосипедов/магазин" |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "defibrillator": { |     "defibrillator": { | ||||||
|  | @ -1069,7 +1064,6 @@ | ||||||
|             "1": { |             "1": { | ||||||
|                 "question": "Вы хотите добавить описание?" |                 "question": "Вы хотите добавить описание?" | ||||||
|             } |             } | ||||||
|         }, |         } | ||||||
|         "name": "Смотровая площадка" |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -122,10 +122,8 @@ | ||||||
|             "thanksForSharing": "Obrigado por compartilhar!", |             "thanksForSharing": "Obrigado por compartilhar!", | ||||||
|             "copiedToClipboard": "Link copiado para a área de transferência", |             "copiedToClipboard": "Link copiado para a área de transferência", | ||||||
|             "addToHomeScreen": "<h3>Adicionar à sua tela inicial</h3>Você pode adicionar facilmente este site à tela inicial do smartphone para uma sensação nativa. Clique no botão 'adicionar à tela inicial' na barra de URL para fazer isso.", |             "addToHomeScreen": "<h3>Adicionar à sua tela inicial</h3>Você pode adicionar facilmente este site à tela inicial do smartphone para uma sensação nativa. Clique no botão 'adicionar à tela inicial' na barra de URL para fazer isso.", | ||||||
|             "intro": "<h3>Compartilhe este mapa</h3> Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:", |             "intro": "<h3>Compartilhe este mapa</h3> Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:" | ||||||
|             "embedIntro": "<h3>Incorpore em seu site</h3>Por favor, incorpore este mapa em seu site.<br>Nós o encorajamos a fazer isso - você nem precisa pedir permissão.<br>É gratuito e sempre será. Quanto mais pessoas usarem isso, mais valioso se tornará." |         } | ||||||
|         }, |  | ||||||
|         "aboutMapcomplete": "<h3>Sobre o MapComplete</h3><p>Com o MapComplete, você pode enriquecer o OpenStreetMap com informações sobre um<b>único tema.</b>Responda a algumas perguntas e, em minutos, suas contribuições estarão disponíveis em todo o mundo! O<b>mantenedor do tema</b>define elementos, questões e linguagens para o tema.</p><h3>Saiba mais</h3><p>MapComplete sempre<b>oferece a próxima etapa</b>para saber mais sobre o OpenStreetMap.</p><ul><li>Quando incorporado em um site, o iframe vincula-se a um MapComplete em tela inteira</li><li>A versão em tela inteira oferece informações sobre o OpenStreetMap</li><li>A visualização funciona sem login, mas a edição requer um login do OSM.</li><li>Se você não estiver conectado, será solicitado que você faça o login</li><li>Depois de responder a uma única pergunta, você pode adicionar novos aponta para o mapa </li><li> Depois de um tempo, as tags OSM reais são mostradas, posteriormente vinculadas ao wiki </li></ul><p></p><br><p>Você percebeu<b>um problema</b>? Você tem uma<b>solicitação de recurso </b>? Quer<b>ajudar a traduzir</b>? Acesse <a href=\"https://github.com/pietervdvn/MapComplete\" target=\"_blank\">o código-fonte</a>ou <a href=\"https: //github.com/pietervdvn/MapComplete / issues \" target=\" _ blank \">rastreador de problemas.</a></p><p>Quer ver<b>seu progresso</b>? Siga a contagem de edição em<a href=\"https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D\" target=\"_blank\">OsmCha</a>.</p>" |  | ||||||
|     }, |     }, | ||||||
|     "index": { |     "index": { | ||||||
|         "pickTheme": "Escolha um tema abaixo para começar.", |         "pickTheme": "Escolha um tema abaixo para começar.", | ||||||
|  | @ -144,13 +142,10 @@ | ||||||
|         "no_reviews_yet": "Não há comentários ainda. Seja o primeiro a escrever um e ajude a abrir os dados e os negócios!", |         "no_reviews_yet": "Não há comentários ainda. Seja o primeiro a escrever um e ajude a abrir os dados e os negócios!", | ||||||
|         "name_required": "É necessário um nome para exibir e criar comentários", |         "name_required": "É necessário um nome para exibir e criar comentários", | ||||||
|         "title_singular": "Um comentário", |         "title_singular": "Um comentário", | ||||||
|         "title": "{count} comentários", |         "title": "{count} comentários" | ||||||
|         "tos": "Se você criar um comentário, você concorda com <a href=\"https://mangrove.reviews/terms\" target=\"_blank\"> o TOS e a política de privacidade de Mangrove.reviews </a>", |  | ||||||
|         "affiliated_reviewer_warning": "(Revisão de afiliados)" |  | ||||||
|     }, |     }, | ||||||
|     "favourite": { |     "favourite": { | ||||||
|         "reload": "Recarregar dados", |         "reload": "Recarregar dados", | ||||||
|         "panelIntro": "<h3>Seu tema pessoal</h3>Ative suas camadas favoritas de todos os temas oficiais", |         "panelIntro": "<h3>Seu tema pessoal</h3>Ative suas camadas favoritas de todos os temas oficiais" | ||||||
|         "loginNeeded": "<h3>Entrar</h3> Um layout pessoal está disponível apenas para usuários do OpenStreetMap" |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,27 +6,6 @@ | ||||||
|         "opening_hours": { |         "opening_hours": { | ||||||
|             "question": "Was sind die Öffnungszeiten von {name}?", |             "question": "Was sind die Öffnungszeiten von {name}?", | ||||||
|             "render": "<h3>Öffnungszeiten</h3>{opening_hours_table(opening_hours)}" |             "render": "<h3>Öffnungszeiten</h3>{opening_hours_table(opening_hours)}" | ||||||
|         }, |  | ||||||
|         "level": { |  | ||||||
|             "mappings": { |  | ||||||
|                 "2": { |  | ||||||
|                     "then": "Ist im ersten Stock" |  | ||||||
|                 }, |  | ||||||
|                 "1": { |  | ||||||
|                     "then": "Ist im Erdgeschoss" |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "render": "Befindet sich im {level}ten Stock", |  | ||||||
|             "question": "In welchem Stockwerk befindet sich dieses Objekt?" |  | ||||||
|         }, |  | ||||||
|         "description": { |  | ||||||
|             "question": "Gibt es noch etwas, das die vorhergehenden Fragen nicht abgedeckt haben? Hier wäre Platz dafür.<br/><span style='font-size: small'>Bitte keine bereits erhobenen Informationen.</span>" |  | ||||||
|         }, |  | ||||||
|         "website": { |  | ||||||
|             "question": "Was ist die Website von {name}?" |  | ||||||
|         }, |  | ||||||
|         "email": { |  | ||||||
|             "question": "Was ist die Mail-Adresse von {name}?" |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -1,30 +1 @@ | ||||||
| { | {} | ||||||
|     "undefined": { |  | ||||||
|         "level": { |  | ||||||
|             "render": "Localizado no {level}o andar", |  | ||||||
|             "mappings": { |  | ||||||
|                 "2": { |  | ||||||
|                     "then": "Localizado no primeiro andar" |  | ||||||
|                 }, |  | ||||||
|                 "1": { |  | ||||||
|                     "then": "Localizado no térreo" |  | ||||||
|                 }, |  | ||||||
|                 "0": { |  | ||||||
|                     "then": "Localizado no subsolo" |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "opening_hours": { |  | ||||||
|             "question": "Qual o horário de funcionamento de {name}?" |  | ||||||
|         }, |  | ||||||
|         "website": { |  | ||||||
|             "question": "Qual o site de {name}?" |  | ||||||
|         }, |  | ||||||
|         "email": { |  | ||||||
|             "question": "Qual o endereço de e-mail de {name}?" |  | ||||||
|         }, |  | ||||||
|         "phone": { |  | ||||||
|             "question": "Qual o número de telefone de {name}?" |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -15,20 +15,6 @@ | ||||||
|         "opening_hours": { |         "opening_hours": { | ||||||
|             "question": "Какое время работы у {name}?", |             "question": "Какое время работы у {name}?", | ||||||
|             "render": "<h3>Часы работы</h3>{opening_hours_table(opening_hours)}" |             "render": "<h3>Часы работы</h3>{opening_hours_table(opening_hours)}" | ||||||
|         }, |  | ||||||
|         "level": { |  | ||||||
|             "mappings": { |  | ||||||
|                 "2": { |  | ||||||
|                     "then": "Расположено на первом этаже" |  | ||||||
|                 }, |  | ||||||
|                 "1": { |  | ||||||
|                     "then": "Расположено на первом этаже" |  | ||||||
|                 }, |  | ||||||
|                 "0": { |  | ||||||
|                     "then": "Расположено под землей" |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "render": "Расположено на {level}ом этаже" |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -1148,13 +1148,6 @@ | ||||||
|                         "human": " gigawatts" |                         "human": " gigawatts" | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }, |  | ||||||
|             "1": { |  | ||||||
|                 "applicableUnits": { |  | ||||||
|                     "0": { |  | ||||||
|                         "human": " meter" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  | @ -956,13 +956,6 @@ | ||||||
|                         "human": " gigawatt" |                         "human": " gigawatt" | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }, |  | ||||||
|             "1": { |  | ||||||
|                 "applicableUnits": { |  | ||||||
|                     "0": { |  | ||||||
|                         "human": " meter" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
							
								
								
									
										920
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										920
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -8,7 +8,7 @@ | ||||||
|   "main": "index.js", |   "main": "index.js", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", |     "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", | ||||||
|     "start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/*.json assets/generated/* assets/layers/*/*.svg assets/tagRendering/*.json assets/themes/*/*.svg assets/themes/*/*.png vendor/* vendor/*/*", |     "start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", | ||||||
|     "test": "ts-node test/TestAll.ts", |     "test": "ts-node test/TestAll.ts", | ||||||
|     "init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean", |     "init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean", | ||||||
|     "add-weblate-upstream": "git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layer-translations/ ; git remote update weblate-layers", |     "add-weblate-upstream": "git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layer-translations/ ; git remote update weblate-layers", | ||||||
|  | @ -20,7 +20,7 @@ | ||||||
|     "generate:layouts": "ts-node scripts/generateLayouts.ts", |     "generate:layouts": "ts-node scripts/generateLayouts.ts", | ||||||
|     "generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts", |     "generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts", | ||||||
|     "generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.20 4.35 51.09 4.56", |     "generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.20 4.35 51.09 4.56", | ||||||
|     "generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../pietervdvn.github.io/natuurpunt_cache/ 50.40 2.1 51.54 6.4 --generate-point-overview nature_reserve,visitor_information_centre", |     "generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../pietervdvn.github.io/natuurpunt_cache/ 50.40 2.1 51.54 6.4", | ||||||
|     "generate:layeroverview": "npm run generate:licenses && echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail", |     "generate:layeroverview": "npm run generate:licenses && echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail", | ||||||
|     "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail", |     "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail", | ||||||
|     "generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push", |     "generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push", | ||||||
|  | @ -65,11 +65,9 @@ | ||||||
|     "escape-html": "^1.0.3", |     "escape-html": "^1.0.3", | ||||||
|     "i18next-client": "^1.11.4", |     "i18next-client": "^1.11.4", | ||||||
|     "jquery": "^3.6.0", |     "jquery": "^3.6.0", | ||||||
|     "jspdf": "^2.3.1", |  | ||||||
|     "latlon2country": "^1.1.3", |     "latlon2country": "^1.1.3", | ||||||
|     "leaflet": "^1.7.1", |     "leaflet": "^1.7.1", | ||||||
|     "leaflet-providers": "^1.10.2", |     "leaflet-providers": "^1.10.2", | ||||||
|     "leaflet-simple-map-screenshoter": "^0.4.4", |  | ||||||
|     "leaflet.markercluster": "^1.4.1", |     "leaflet.markercluster": "^1.4.1", | ||||||
|     "libphonenumber": "0.0.10", |     "libphonenumber": "0.0.10", | ||||||
|     "libphonenumber-js": "^1.7.55", |     "libphonenumber-js": "^1.7.55", | ||||||
|  | @ -83,6 +81,7 @@ | ||||||
|     "postcss": "^7.0.36", |     "postcss": "^7.0.36", | ||||||
|     "prompt-sync": "^4.2.0", |     "prompt-sync": "^4.2.0", | ||||||
|     "sharp": "^0.27.0", |     "sharp": "^0.27.0", | ||||||
|  |     "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2", | ||||||
|     "tslint": "^6.1.3" |     "tslint": "^6.1.3" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  | @ -92,7 +91,6 @@ | ||||||
|     "fs": "0.0.1-security", |     "fs": "0.0.1-security", | ||||||
|     "marked": "^2.0.0", |     "marked": "^2.0.0", | ||||||
|     "read-file": "^0.2.0", |     "read-file": "^0.2.0", | ||||||
|     "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4", |  | ||||||
|     "ts-node": "^9.0.0", |     "ts-node": "^9.0.0", | ||||||
|     "ts-node-dev": "^1.0.0-pre.63", |     "ts-node-dev": "^1.0.0-pre.63", | ||||||
|     "tslint-no-circular-imports": "^0.7.0", |     "tslint-no-circular-imports": "^0.7.0", | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ import BaseUIElement from "./UI/BaseUIElement"; | ||||||
| import Table from "./UI/Base/Table"; | import Table from "./UI/Base/Table"; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| const connection = new OsmConnection(false, false, new UIEventSource<string>(undefined), ""); | const connection = new OsmConnection(false, new UIEventSource<string>(undefined), ""); | ||||||
| 
 | 
 | ||||||
| let rendered = false; | let rendered = false; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| /** | /** | ||||||
|  * Generates a collection of geojson files based on an overpass query for a given theme |  * Generates a collection of geojson files based on an overpass query for a given theme | ||||||
|  */ |  */ | ||||||
| import {Utils} from "../Utils"; | import {TileRange, Utils} from "../Utils"; | ||||||
| 
 | 
 | ||||||
| Utils.runningFromConsole = true | Utils.runningFromConsole = true | ||||||
| import {Overpass} from "../Logic/Osm/Overpass"; | import {Overpass} from "../Logic/Osm/Overpass"; | ||||||
|  | @ -17,8 +17,6 @@ import MetaTagging from "../Logic/MetaTagging"; | ||||||
| import LayerConfig from "../Customizations/JSON/LayerConfig"; | import LayerConfig from "../Customizations/JSON/LayerConfig"; | ||||||
| import {GeoOperations} from "../Logic/GeoOperations"; | import {GeoOperations} from "../Logic/GeoOperations"; | ||||||
| import {UIEventSource} from "../Logic/UIEventSource"; | import {UIEventSource} from "../Logic/UIEventSource"; | ||||||
| import * as fs from "fs"; |  | ||||||
| import {TileRange} from "../Models/TileRange"; |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| function createOverpassObject(theme: LayoutConfig) { | function createOverpassObject(theme: LayoutConfig) { | ||||||
|  | @ -141,7 +139,7 @@ async function downloadExtraData(theme: LayoutConfig)/* : any[] */ { | ||||||
|     return allFeatures; |     return allFeatures; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) { | async function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) { | ||||||
|     let processed = 0; |     let processed = 0; | ||||||
|     const layerIndex = theme.LayerIndex(); |     const layerIndex = theme.LayerIndex(); | ||||||
|     for (let x = r.xstart; x <= r.xend; x++) { |     for (let x = r.xstart; x <= r.xend; x++) { | ||||||
|  | @ -213,9 +211,8 @@ function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extra | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) { | async function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) { | ||||||
|     const z = r.zoomlevel; |     const z = r.zoomlevel; | ||||||
|     const generated = {} // layer --> x --> y[]
 |  | ||||||
|     for (let x = r.xstart; x <= r.xend; x++) { |     for (let x = r.xstart; x <= r.xend; x++) { | ||||||
|         for (let y = r.ystart; y <= r.yend; y++) { |         for (let y = r.ystart; y <= r.yend; y++) { | ||||||
|             const file = readFileSync(geoJsonName(targetdir + ".unfiltered", x, y, z), "UTF8") |             const file = readFileSync(geoJsonName(targetdir + ".unfiltered", x, y, z), "UTF8") | ||||||
|  | @ -230,8 +227,10 @@ function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) { | ||||||
|                     .filter(f => f._matching_layer_id === layer.id) |                     .filter(f => f._matching_layer_id === layer.id) | ||||||
|                     .filter(f => { |                     .filter(f => { | ||||||
|                         const isShown = layer.isShown.GetRenderValue(f.properties).txt |                         const isShown = layer.isShown.GetRenderValue(f.properties).txt | ||||||
|                         return isShown !== "no"; |                         if (isShown === "no") { | ||||||
| 
 |                             return false; | ||||||
|  |                         } | ||||||
|  |                         return true; | ||||||
|                     }) |                     }) | ||||||
|                 const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z); |                 const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z); | ||||||
|                 console.log(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")") |                 console.log(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")") | ||||||
|  | @ -240,66 +239,18 @@ function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) { | ||||||
|                     continue; |                     continue; | ||||||
|                 } |                 } | ||||||
|                 writeFileSync(new_path, JSON.stringify(geojson, null, " ")) |                 writeFileSync(new_path, JSON.stringify(geojson, null, " ")) | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|                 if (generated[layer.id] === undefined) { |  | ||||||
|                     generated[layer.id] = {} |  | ||||||
|                 } |  | ||||||
|                 if (generated[layer.id][x] === undefined) { |  | ||||||
|                     generated[layer.id][x] = [] |  | ||||||
|                 } |  | ||||||
|                 generated[layer.id][x].push(y) |  | ||||||
| 
 | 
 | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|     for (const layer of theme.layers) { |  | ||||||
|         const id = layer.id |  | ||||||
|         const loaded = generated[id] |  | ||||||
|         if(loaded === undefined){ |  | ||||||
|             console.log("No features loaded for layer ",id) |  | ||||||
|             continue; |  | ||||||
|         } |  | ||||||
|         writeFileSync(targetdir + "_" + id + "_overview.json", JSON.stringify(loaded)) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| async function createOverview(targetdir: string, r: TileRange, z: number, layername: string) { |  | ||||||
|     const allFeatures = [] |  | ||||||
|     for (let x = r.xstart; x <= r.xend; x++) { |  | ||||||
|         for (let y = r.ystart; y <= r.yend; y++) { |  | ||||||
|             const read_path = geoJsonName(targetdir + "_" + layername, x, y, z); |  | ||||||
|             if (!fs.existsSync(read_path)) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             const features = JSON.parse(fs.readFileSync(read_path, "UTF-8")).features |  | ||||||
|             const pointsOnly = features.map(f => { |  | ||||||
|                  |  | ||||||
|                 f.properties["_last_edit:timestamp"] = "1970-01-01" |  | ||||||
|                  |  | ||||||
|                 if (f.geometry.type === "Point") { |  | ||||||
|                     return f |  | ||||||
|                 } else { |  | ||||||
|                     return GeoOperations.centerpoint(f) |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|             }) |  | ||||||
|             allFeatures.push(...pointsOnly) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const geojson = { |  | ||||||
|         "type": "FeatureCollection", |  | ||||||
|         "features": allFeatures |  | ||||||
|     } |  | ||||||
|     writeFileSync(targetdir + "_" + layername + "_points.geojson", JSON.stringify(geojson, null, " ")) |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| async function main(args: string[]) { | async function main(args: string[]) { | ||||||
| 
 | 
 | ||||||
|     if (args.length == 0) { |     if (args.length == 0) { | ||||||
|         console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name]") |         console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1") | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|     const themeName = args[0] |     const themeName = args[0] | ||||||
|  | @ -334,18 +285,8 @@ async function main(args: string[]) { | ||||||
|     } while (failed > 0) |     } while (failed > 0) | ||||||
| 
 | 
 | ||||||
|     const extraFeatures = await downloadExtraData(theme); |     const extraFeatures = await downloadExtraData(theme); | ||||||
|     postProcess(targetdir, tileRange, theme, extraFeatures) |     await postProcess(targetdir, tileRange, theme, extraFeatures) | ||||||
|     splitPerLayer(targetdir, tileRange, theme) |     await splitPerLayer(targetdir, tileRange, theme) | ||||||
| 
 |  | ||||||
|     if (args[7] === "--generate-point-overview") { |  | ||||||
|         const targetLayers = args[8].split(",") |  | ||||||
|         for (const targetLayer of targetLayers) { |  | ||||||
|             if (!theme.layers.some(l => l.id === targetLayer)) { |  | ||||||
|                 throw "Target layer " + targetLayer + " not found, did you mistype the name? Found layers are: " + theme.layers.map(l => l.id).join(",") |  | ||||||
|             } |  | ||||||
|             createOverview(targetdir, tileRange, zoomlevel, targetLayer) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import LayerConfig from "../Customizations/JSON/LayerConfig"; | ||||||
| import * as licenses from "../assets/generated/license_info.json" | import * as licenses from "../assets/generated/license_info.json" | ||||||
| import LayoutConfig from "../Customizations/JSON/LayoutConfig"; | import LayoutConfig from "../Customizations/JSON/LayoutConfig"; | ||||||
| import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson"; | import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson"; | ||||||
|  | import {Translation} from "../UI/i18n/Translation"; | ||||||
| import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson"; | import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson"; | ||||||
| import AllKnownLayers from "../Customizations/AllKnownLayers"; | import AllKnownLayers from "../Customizations/AllKnownLayers"; | ||||||
| 
 | 
 | ||||||
|  | @ -76,6 +77,63 @@ class LayerOverviewUtils { | ||||||
|         return errorCount |         return errorCount | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     validateTranslationCompletenessOfObject(object: any, expectedLanguages: string[], context: string) { | ||||||
|  |         const missingTranlations = [] | ||||||
|  |         const translations: { tr: Translation, context: string }[] = []; | ||||||
|  |         const queue: { object: any, context: string }[] = [{object: object, context: context}] | ||||||
|  | 
 | ||||||
|  |         while (queue.length > 0) { | ||||||
|  |             const item = queue.pop(); | ||||||
|  |             const o = item.object | ||||||
|  |             for (const key in o) { | ||||||
|  |                 const v = o[key]; | ||||||
|  |                 if (v === undefined) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |                 if (v instanceof Translation || v?.translations !== undefined) { | ||||||
|  |                     translations.push({tr: v, context: item.context}); | ||||||
|  |                 } else if ( | ||||||
|  |                     ["string", "function", "boolean", "number"].indexOf(typeof (v)) < 0) { | ||||||
|  |                     queue.push({object: v, context: item.context + "." + key}) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const missing = {} | ||||||
|  |         const present = {} | ||||||
|  |         for (const ln of expectedLanguages) { | ||||||
|  |             missing[ln] = 0; | ||||||
|  |             present[ln] = 0; | ||||||
|  |             for (const translation of translations) { | ||||||
|  |                 if (translation.tr.translations["*"] !== undefined) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |                 const txt = translation.tr.translations[ln]; | ||||||
|  |                 const isMissing = txt === undefined || txt === "" || txt.toLowerCase().indexOf("todo") >= 0; | ||||||
|  |                 if (isMissing) { | ||||||
|  |                     missingTranlations.push(`${translation.context},${ln},${translation.tr.txt}`) | ||||||
|  |                     missing[ln]++ | ||||||
|  |                 } else { | ||||||
|  |                     present[ln]++; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let message = `Translation completeness for ${context}` | ||||||
|  |         let isComplete = true; | ||||||
|  |         for (const ln of expectedLanguages) { | ||||||
|  |             const amiss = missing[ln]; | ||||||
|  |             const ok = present[ln]; | ||||||
|  |             const total = amiss + ok; | ||||||
|  |             message += ` ${ln}: ${ok}/${total}` | ||||||
|  |             if (ok !== total) { | ||||||
|  |                 isComplete = false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return missingTranlations | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     main(args: string[]) { |     main(args: string[]) { | ||||||
| 
 | 
 | ||||||
|         const lt = this.loadThemesAndLayers(); |         const lt = this.loadThemesAndLayers(); | ||||||
|  | @ -102,6 +160,7 @@ class LayerOverviewUtils { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let themeErrorCount = [] |         let themeErrorCount = [] | ||||||
|  |         let missingTranslations = [] | ||||||
|         for (const themeFile of themeFiles) { |         for (const themeFile of themeFiles) { | ||||||
|             if (typeof themeFile.language === "string") { |             if (typeof themeFile.language === "string") { | ||||||
|                 themeErrorCount.push("The theme " + themeFile.id + " has a string as language. Please use a list of strings") |                 themeErrorCount.push("The theme " + themeFile.id + " has a string as language. Please use a list of strings") | ||||||
|  | @ -110,6 +169,10 @@ class LayerOverviewUtils { | ||||||
|                 if (typeof layer === "string") { |                 if (typeof layer === "string") { | ||||||
|                     if (!knownLayerIds.has(layer)) { |                     if (!knownLayerIds.has(layer)) { | ||||||
|                         themeErrorCount.push(`Unknown layer id: ${layer} in theme ${themeFile.id}`) |                         themeErrorCount.push(`Unknown layer id: ${layer} in theme ${themeFile.id}`) | ||||||
|  |                     } else { | ||||||
|  |                         const layerConfig = knownLayerIds.get(layer); | ||||||
|  |                         missingTranslations.push(...this.validateTranslationCompletenessOfObject(layerConfig, themeFile.language, "Layer " + layer)) | ||||||
|  | 
 | ||||||
|                     } |                     } | ||||||
|                 } else if (layer.builtin !== undefined) { |                 } else if (layer.builtin !== undefined) { | ||||||
|                     let names = layer.builtin; |                     let names = layer.builtin; | ||||||
|  | @ -134,6 +197,7 @@ class LayerOverviewUtils { | ||||||
|                 .filter(l => typeof l != "string") // We remove all the builtin layer references as they don't work with ts-node for some weird reason
 |                 .filter(l => typeof l != "string") // We remove all the builtin layer references as they don't work with ts-node for some weird reason
 | ||||||
|                 .filter(l => l.builtin === undefined) |                 .filter(l => l.builtin === undefined) | ||||||
| 
 | 
 | ||||||
|  |             missingTranslations.push(...this.validateTranslationCompletenessOfObject(themeFile, themeFile.language, "Theme " + themeFile.id)) | ||||||
| 
 | 
 | ||||||
|             try { |             try { | ||||||
|                 const theme = new LayoutConfig(themeFile, true, "test") |                 const theme = new LayoutConfig(themeFile, true, "test") | ||||||
|  | @ -145,6 +209,11 @@ class LayerOverviewUtils { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         if (missingTranslations.length > 0) { | ||||||
|  |             console.log(missingTranslations.length, "missing translations") | ||||||
|  |             writeFileSync("missing_translations.txt", missingTranslations.join("\n")) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         if (layerErrorCount.length + themeErrorCount.length == 0) { |         if (layerErrorCount.length + themeErrorCount.length == 0) { | ||||||
|             console.log("All good!") |             console.log("All good!") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										33
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										33
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -7,11 +7,6 @@ import {UIEventSource} from "./Logic/UIEventSource"; | ||||||
| import {Tag} from "./Logic/Tags/Tag"; | import {Tag} from "./Logic/Tags/Tag"; | ||||||
| import {QueryParameters} from "./Logic/Web/QueryParameters"; | import {QueryParameters} from "./Logic/Web/QueryParameters"; | ||||||
| import {Translation} from "./UI/i18n/Translation"; | import {Translation} from "./UI/i18n/Translation"; | ||||||
| import LocationInput from "./UI/Input/LocationInput"; |  | ||||||
| import Loc from "./Models/Loc"; |  | ||||||
| import {VariableUiElement} from "./UI/Base/VariableUIElement"; |  | ||||||
| import LengthInput from "./UI/Input/LengthInput"; |  | ||||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; |  | ||||||
| /*import ValidatedTextField from "./UI/Input/ValidatedTextField"; | /*import ValidatedTextField from "./UI/Input/ValidatedTextField"; | ||||||
| import Combine from "./UI/Base/Combine"; | import Combine from "./UI/Base/Combine"; | ||||||
| import {VariableUiElement} from "./UI/Base/VariableUIElement"; | import {VariableUiElement} from "./UI/Base/VariableUIElement"; | ||||||
|  | @ -153,17 +148,19 @@ function TestMiniMap() { | ||||||
|     featureSource.ping() |     featureSource.ping() | ||||||
| } | } | ||||||
| //*/
 | //*/
 | ||||||
| 
 | QueryParameters.GetQueryParameter("test", "true").setData("true") | ||||||
| const loc = new UIEventSource<Loc>({ | State.state= new State(undefined) | ||||||
|     zoom: 24, | const id = "node/5414688303" | ||||||
|     lat: 51.21043, | State.state.allElements.addElementById(id, new UIEventSource<any>({id: id})) | ||||||
|     lon: 3.21389 | new Combine([ | ||||||
|  |     new DeleteWizard(id, { | ||||||
|  |         noDeleteOptions: [ | ||||||
|  |             { | ||||||
|  |                 if:[ new Tag("access","private")], | ||||||
|  |                 then: new Translation({ | ||||||
|  |                     en: "Very private! Delete now or me send lawfull lawyer" | ||||||
|                 }) |                 }) | ||||||
| const li = new LengthInput( |             } | ||||||
|     AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource<string | string[]>("map","photo")), |         ] | ||||||
|     loc |     }), | ||||||
| ) | ]).AttachTo("maindiv") | ||||||
|     li.SetStyle("height: 30rem; background: aliceblue;") |  | ||||||
|         .AttachTo("maindiv") |  | ||||||
| 
 |  | ||||||
| new VariableUiElement(li.GetValue().map(v => JSON.stringify(v, null, "  "))).AttachTo("extradiv") |  | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ export default class OsmConnectionSpec extends T { | ||||||
|         super("OsmConnectionSpec-test", [ |         super("OsmConnectionSpec-test", [ | ||||||
|             ["login on dev", |             ["login on dev", | ||||||
|                 () => { |                 () => { | ||||||
|                    const osmConn = new OsmConnection(false,false, |                    const osmConn = new OsmConnection(false, | ||||||
|                         new UIEventSource<string>(undefined), |                         new UIEventSource<string>(undefined), | ||||||
|                         "Unit test", |                         "Unit test", | ||||||
|                         true, |                         true, | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								tslint.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tslint.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | { | ||||||
|  |     "defaultSeverity": "error", | ||||||
|  |     "extends": [ | ||||||
|  |         "tslint:recommended", | ||||||
|  |         "tslint-no-circular-imports" | ||||||
|  |     ], | ||||||
|  |     "jsRules": {}, | ||||||
|  |     "rules": {}, | ||||||
|  |     "rulesDirectory": [] | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue