forked from MapComplete/MapComplete
		
	Add custom javascript snippets to calculate tags
This commit is contained in:
		
							parent
							
								
									3e130ebe80
								
							
						
					
					
						commit
						f124d9ded7
					
				
					 17 changed files with 799 additions and 14649 deletions
				
			
		|  | @ -28,6 +28,7 @@ export default class LayerConfig { | |||
|     name: Translation | ||||
|     description: Translation; | ||||
|     source: SourceConfig; | ||||
|     calculatedTags: [string, string][] | ||||
|     doNotDownload: boolean; | ||||
|     passAllFeatures: boolean; | ||||
|     minzoom: number; | ||||
|  | @ -51,8 +52,9 @@ export default class LayerConfig { | |||
|     }[]; | ||||
| 
 | ||||
|     tagRenderings: TagRenderingConfig []; | ||||
|      | ||||
| 
 | ||||
|     constructor(json: LayerConfigJson, | ||||
|                 official: boolean= true, | ||||
|                 context?: string) { | ||||
|         context = context + "." + json.id; | ||||
|         const self = this; | ||||
|  | @ -65,9 +67,9 @@ export default class LayerConfig { | |||
|             // @ts-ignore
 | ||||
|             legacy = FromJSON.Tag(json["overpassTags"], context + ".overpasstags"); | ||||
|         } | ||||
|         if(json.source !== undefined){ | ||||
|             if (legacy !== undefined ) { | ||||
|                 throw context+"Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined" | ||||
|         if (json.source !== undefined) { | ||||
|             if (legacy !== undefined) { | ||||
|                 throw context + "Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined" | ||||
|             } | ||||
| 
 | ||||
|             let osmTags: TagsFilter = legacy; | ||||
|  | @ -81,15 +83,22 @@ export default class LayerConfig { | |||
|                 geojsonSource: json.source["geoJsonSource"], | ||||
|                 overpassScript: json.source["overpassScript"], | ||||
|             }); | ||||
|         }else{ | ||||
|         } else { | ||||
|             this.source = new SourceConfig({ | ||||
|                 osmTags : legacy | ||||
|                 osmTags: legacy | ||||
|             }) | ||||
|         } | ||||
|          | ||||
|        | ||||
| 
 | ||||
| 
 | ||||
|         this.calculatedTags = undefined; | ||||
|         if (json.calculatedTags !== undefined) { | ||||
|             console.warn("Unofficial theme with custom javascript! This is a security risk") | ||||
|             this.calculatedTags = []; | ||||
|             for (const key in json.calculatedTags) { | ||||
|                 this.calculatedTags.push([key, json.calculatedTags[key]]) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.doNotDownload = json.doNotDownload ?? false; | ||||
|         this.passAllFeatures = json.passAllFeatures ?? false; | ||||
|         this.minzoom = json.minzoom ?? 0; | ||||
|  | @ -139,10 +148,10 @@ export default class LayerConfig { | |||
|                     if (typeof renderingJson === "string") { | ||||
| 
 | ||||
|                         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}` | ||||
|                             } | ||||
|                              | ||||
| 
 | ||||
|                             return new TagRenderingConfig("questions", undefined) | ||||
|                         } | ||||
| 
 | ||||
|  | @ -203,6 +212,13 @@ export default class LayerConfig { | |||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public CustomCodeSnippets(): string[]{ | ||||
|         if(this.calculatedTags === undefined){ | ||||
|             return [] | ||||
|         } | ||||
|          | ||||
|         return this.calculatedTags.map(code => code[1]); | ||||
|     } | ||||
| 
 | ||||
|     public AddRoamingRenderings(addAll: { | ||||
|         tagRenderings: TagRenderingConfig[], | ||||
|  |  | |||
|  | @ -41,6 +41,11 @@ export interface LayerConfigJson { | |||
|      */ | ||||
|     source: {osmTags: AndOrTagConfigJson | string} | {geoJsonSource: string} | {overpassScript: string} | ||||
| 
 | ||||
|     /** | ||||
|      * A dictionary of 'key': 'js-expression'. These js-expressions will be calculated for every feature, giving extra tags to work with in the rest of the pipieline | ||||
|      */ | ||||
|     calculatedTags? : any; | ||||
| 
 | ||||
|     /** | ||||
|      * If set, this layer will not query overpass; but it'll still match the tags above which are by chance returned by other layers.  | ||||
|      * Works well together with 'passAllFeatures', to add decoration | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ export default class LayoutConfig { | |||
|     }; | ||||
| 
 | ||||
|     public readonly hideFromOverview: boolean; | ||||
|     public readonly lockLocation: boolean | [[number,number],[number, number]]; | ||||
|     public readonly lockLocation: boolean | [[number, number], [number, number]]; | ||||
|     public readonly enableUserBadge: boolean; | ||||
|     public readonly enableShareScreen: boolean; | ||||
|     public readonly enableMoreQuests: boolean; | ||||
|  | @ -39,10 +39,12 @@ export default class LayoutConfig { | |||
|     public readonly enableLayers: boolean; | ||||
|     public readonly enableSearch: boolean; | ||||
|     public readonly enableGeolocation: boolean; | ||||
|     private readonly _official : boolean; | ||||
|     public readonly enableBackgroundLayerSelection: boolean; | ||||
|     public readonly customCss?: string; | ||||
| 
 | ||||
|     constructor(json: LayoutConfigJson, context?: string) { | ||||
|     constructor(json: LayoutConfigJson, official=true, context?: string) { | ||||
|         this._official = official; | ||||
|         this.id = json.id; | ||||
|         context = (context ?? "") + "." + this.id; | ||||
|         this.maintainer = json.maintainer; | ||||
|  | @ -54,7 +56,7 @@ export default class LayoutConfig { | |||
|         } else { | ||||
|             this.language = json.language; | ||||
|         } | ||||
|         if(this.language.length == 0){ | ||||
|         if (this.language.length == 0) { | ||||
|             throw "No languages defined. Define at least one language" | ||||
|         } | ||||
|         if (json.title === undefined) { | ||||
|  | @ -66,7 +68,7 @@ export default class LayoutConfig { | |||
|         this.title = new Translation(json.title, context + ".title"); | ||||
|         this.description = new Translation(json.description, context + ".description"); | ||||
|         this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, context + ".shortdescription"); | ||||
|         this.descriptionTail = json.descriptionTail === undefined ? new Translation({"*": ""}, context+".descriptionTail") : new Translation(json.descriptionTail, context + ".descriptionTail"); | ||||
|         this.descriptionTail = json.descriptionTail === undefined ? new Translation({"*": ""}, context + ".descriptionTail") : new Translation(json.descriptionTail, context + ".descriptionTail"); | ||||
|         this.icon = json.icon; | ||||
|         this.socialImage = json.socialImage; | ||||
|         this.startZoom = json.startZoom; | ||||
|  | @ -79,7 +81,7 @@ export default class LayoutConfig { | |||
|                         return SharedTagRenderings.SharedTagRendering[tr]; | ||||
|                     } | ||||
|                 } | ||||
|                 return new TagRenderingConfig(tr, undefined,`${this.id}.roaming_renderings[${i}]`); | ||||
|                 return new TagRenderingConfig(tr, undefined, `${this.id}.roaming_renderings[${i}]`); | ||||
|             } | ||||
|         ); | ||||
|         this.defaultBackgroundId = json.defaultBackgroundId; | ||||
|  | @ -104,32 +106,31 @@ export default class LayoutConfig { | |||
|             } | ||||
| 
 | ||||
|             // @ts-ignore
 | ||||
|             return new LayerConfig(layer, `${this.id}.layers[${i}]`) | ||||
|             return new LayerConfig(layer, official,`${this.id}.layers[${i}]`) | ||||
|         }); | ||||
|          | ||||
| 
 | ||||
|         // ALl the layers are constructed, let them share tags in now!
 | ||||
|         const roaming : {r, source: LayerConfig}[] = [] | ||||
|         const roaming: { r, source: LayerConfig }[] = [] | ||||
|         for (const layer of this.layers) { | ||||
|             roaming.push({r: layer.GetRoamingRenderings(), source:layer}); | ||||
|             roaming.push({r: layer.GetRoamingRenderings(), source: layer}); | ||||
|         } | ||||
| 
 | ||||
|         for (const layer of this.layers) { | ||||
|             for (const r of roaming) { | ||||
|                 if(r.source == layer){ | ||||
|                 if (r.source == layer) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 layer.AddRoamingRenderings(r.r); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         for(const layer of this.layers) { | ||||
| 
 | ||||
|         for (const layer of this.layers) { | ||||
|             layer.AddRoamingRenderings( | ||||
|                 { | ||||
|                     titleIcons:[], | ||||
|                     titleIcons: [], | ||||
|                     iconOverlays: [], | ||||
|                     tagRenderings: this.roamingRenderings | ||||
|                 }   | ||||
|                  | ||||
|                 } | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|  | @ -151,8 +152,8 @@ export default class LayoutConfig { | |||
| 
 | ||||
|         this.hideFromOverview = json.hideFromOverview ?? false; | ||||
|         // @ts-ignore
 | ||||
|         if(json.hideInOverview){ | ||||
|             throw "The json for "+this.id+" contains a 'hideInOverview'. Did you mean hideFromOverview instead?" | ||||
|         if (json.hideInOverview) { | ||||
|             throw "The json for " + this.id + " contains a 'hideInOverview'. Did you mean hideFromOverview instead?" | ||||
|         } | ||||
|         this.lockLocation = json.lockLocation ?? false; | ||||
|         this.enableUserBadge = json.enableUserBadge ?? true; | ||||
|  | @ -166,4 +167,19 @@ export default class LayoutConfig { | |||
|         this.customCss = json.customCss; | ||||
|     } | ||||
| 
 | ||||
|     public CustomCodeSnippets(): string[] { | ||||
|         if(this._official){ | ||||
|             return []; | ||||
|         } | ||||
|         const msg = "<br/><b>This layout uses <span class='alert'>custom javascript</span>, loaded for the wide internet. The code is printed below, please report suspicious code on the issue tracker of MapComplete:</b><br/>" | ||||
|         const custom = []; | ||||
|         for (const layer of this.layers) { | ||||
|             custom.push(...layer.CustomCodeSnippets().map(code => code+"<br />")) | ||||
|         } | ||||
|         if (custom.length === 0) { | ||||
|             return custom; | ||||
|         } | ||||
|         custom.splice(0, 0, msg); | ||||
|         return custom; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										22
									
								
								Docs/CalculatingExtraTags.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Docs/CalculatingExtraTags.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| # Extra, automatically created tags | ||||
| 
 | ||||
| In some cases, it is useful to have some tags calculated based on other properties. | ||||
| 
 | ||||
| Some useful tags are available by default (e.g. `_lat`, `_lon`, `_country`) and are always available (have a lookt at [CalculatedTags.md](CalculatedTags.md) to see an overview). | ||||
| 
 | ||||
| It is also possible to calculate your own tags - but this requires some javascript knowledge.  | ||||
| 
 | ||||
| Before proceeding, some warnings: | ||||
| 
 | ||||
| - **DO NOT DO THIS AS BEGINNER** | ||||
| - **Only do this if all other techniques fail**. This should _not_ be done to create a rendering effect, only to calculate a specific vaue | ||||
| - **THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES**. As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs. | ||||
| 
 | ||||
| In the layer object, add a field `calculatedTags`, e.g.: | ||||
| 
 | ||||
| ``` | ||||
|   "calculatedTags": { | ||||
|     "_someKey": "javascript-expression", | ||||
|     "name": "tags.name ?? tags.ref ?? tags.operator" | ||||
|   } | ||||
| ``` | ||||
|  | @ -209,7 +209,7 @@ export class InitUiElements { | |||
|                 hashFromLocalStorage.setData(hash); | ||||
|                 dedicatedHashFromLocalStorage.setData(hash); | ||||
|             } | ||||
|             const layoutToUse = new LayoutConfig(JSON.parse(atob(hash))); | ||||
|             const layoutToUse = new LayoutConfig(JSON.parse(atob(hash)), false); | ||||
|             userLayoutParam.setData(layoutToUse.id); | ||||
|             return layoutToUse; | ||||
|         } catch (e) { | ||||
|  | @ -380,7 +380,6 @@ export class InitUiElements { | |||
|                 return flayers; | ||||
|             }); | ||||
| 
 | ||||
| 
 | ||||
|         const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap); | ||||
|         State.state.layerUpdater = updater; | ||||
|         const source = new FeaturePipeline(state.filteredLayers, updater, state.layoutToUse, state.changes, state.locationControl); | ||||
|  | @ -399,7 +398,8 @@ export class InitUiElements { | |||
|                 } | ||||
| 
 | ||||
|             }) | ||||
|             MetaTagging.addMetatags(featuresFreshness); | ||||
| 
 | ||||
|             MetaTagging.addMetatags(featuresFreshness, state.layoutToUse.data.layers); | ||||
|         }) | ||||
| 
 | ||||
|         new ShowDataLayer(source.features, State.state.leafletMap, | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ export default class InstalledThemes { | |||
|                     try { | ||||
|                         const json = atob(customLayout.data); | ||||
|                         const layout = new LayoutConfig( | ||||
|                             JSON.parse(json)); | ||||
|                             JSON.parse(json), false); | ||||
|                         installedThemes.push({ | ||||
|                             layout: layout, | ||||
|                             definition: customLayout.data | ||||
|  |  | |||
|  | @ -21,6 +21,15 @@ export class GeoOperations { | |||
|         return coordinates; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the distance between the two points in kilometers | ||||
|      * @param lonlat0 | ||||
|      * @param lonlat1 | ||||
|      */ | ||||
|     static distanceBetween(lonlat0: [number,number], lonlat1:[number, number]){ | ||||
|         return turf.distance(lonlat0, lonlat1) | ||||
|     } | ||||
| 
 | ||||
|     static featureIsContainedInAny(feature: any, | ||||
|                                    shouldNotContain: any[], | ||||
|                                    maxOverlapPercentage: number): boolean { | ||||
|  |  | |||
|  | @ -1,40 +1,46 @@ | |||
| import {GeoOperations} from "./GeoOperations"; | ||||
| import State from "../State"; | ||||
| import opening_hours from "opening_hours"; | ||||
| import {Or} from "./Or"; | ||||
| import {Utils} from "../Utils"; | ||||
| import {UIElement} from "../UI/UIElement"; | ||||
| import Combine from "../UI/Base/Combine"; | ||||
| import {Tag} from "./Tag"; | ||||
| import {And} from "./And"; | ||||
| import LayerConfig from "../Customizations/JSON/LayerConfig"; | ||||
| import SimpleMetaTagger from "./SimpleMetaTagger"; | ||||
| 
 | ||||
| class SimpleMetaTagger { | ||||
|     public readonly keys: string[]; | ||||
|     public readonly doc: string; | ||||
|     private readonly _f: (feature: any, index: number, freshness: Date) => void; | ||||
| export class ExtraFunction { | ||||
| 
 | ||||
|     constructor(keys: string[], doc: string, f: ((feature: any, index: number, freshness: Date) => void)) { | ||||
|         this.keys = keys; | ||||
|         this.doc = doc; | ||||
|         this._f = f; | ||||
|         for (const key of keys) { | ||||
|             if (!key.startsWith('_')) { | ||||
|                 throw `Incorrect metakey ${key}: it should start with underscore (_)` | ||||
|     static readonly doc: string = "When the feature is downloaded, some extra tags can be calculated by a javascript snippet. The feature is passed as 'feat'; there are a few functions available on it to handle it - apart from 'feat.tags' which is a classic object containing all the tags." | ||||
|     private static DistanceToFunc = new ExtraFunction( | ||||
|         "distanceTo", | ||||
|         "Calculates the distance between the feature and a specified point", | ||||
|         ["longitude", "latitude"], | ||||
|         (feature) => { | ||||
|             return (lon, lat) => { | ||||
|                 // Feature._lon and ._lat is conveniently place by one of the other metatags
 | ||||
|                 return GeoOperations.distanceBetween([lon, lat], [feature._lon, feature._lat]); | ||||
|             } | ||||
|         } | ||||
|     ) | ||||
|     private static readonly allFuncs : ExtraFunction[] = [ExtraFunction.DistanceToFunc]; | ||||
|     private readonly _name: string; | ||||
|     private readonly _args: string[]; | ||||
|     private readonly _doc: string; | ||||
|     private readonly _f: (feat: any) => any; | ||||
| 
 | ||||
|     constructor(name: string, doc: string, args: string[], f: ((feat: any) => any)) { | ||||
|         this._name = name; | ||||
|         this._doc = doc; | ||||
|         this._args = args; | ||||
|         this._f = f; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     addMetaTags(features: { feature: any, freshness: Date }[]) { | ||||
|         for (let i = 0; i < features.length; i++) { | ||||
|             let feature = features[i]; | ||||
|             this._f(feature.feature, i, feature.freshness); | ||||
|     public static FullPatchFeature(feature) { | ||||
|         for (const func of ExtraFunction.allFuncs) { | ||||
|             func.PatchFeature(feature); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public PatchFeature(feature: any) { | ||||
|         feature[this._name] = this._f(feature); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... | ||||
|  * | ||||
|  | @ -43,282 +49,13 @@ class SimpleMetaTagger { | |||
| export default class MetaTagging { | ||||
| 
 | ||||
| 
 | ||||
|     static coder: any; | ||||
|     private static latlon = new SimpleMetaTagger(["_lat", "_lon"], "The latitude and longitude of the point (or centerpoint in the case of a way/area)", | ||||
|         (feature => { | ||||
|             const centerPoint = GeoOperations.centerpoint(feature); | ||||
|             const lat = centerPoint.geometry.coordinates[1]; | ||||
|             const lon = centerPoint.geometry.coordinates[0]; | ||||
|             feature.properties["_lat"] = "" + lat; | ||||
|             feature.properties["_lon"] = "" + lon; | ||||
|         }) | ||||
|     ); | ||||
|     private static surfaceArea = new SimpleMetaTagger( | ||||
|         ["_surface", "_surface:ha"], "The surface area of the feature, in square meters and in hectare. Not set on points and ways", | ||||
|         (feature => { | ||||
|             const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature); | ||||
|             feature.properties["_surface"] = "" + sqMeters; | ||||
|             feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10; | ||||
| 
 | ||||
|         }) | ||||
|     ); | ||||
|     private static country = new SimpleMetaTagger( | ||||
|         ["_country"], "The country code of the property (with latlon2country)", | ||||
|         feature => { | ||||
| 
 | ||||
| 
 | ||||
|             let centerPoint: any = GeoOperations.centerpoint(feature); | ||||
|             const lat = centerPoint.geometry.coordinates[1]; | ||||
|             const lon = centerPoint.geometry.coordinates[0]; | ||||
| 
 | ||||
|             MetaTagging.GetCountryCodeFor(lon, lat, (countries) => { | ||||
|                 try { | ||||
|                     feature.properties["_country"] = countries[0].trim().toLowerCase(); | ||||
|                     const tagsSource = State.state.allElements.getEventSourceFor(feature); | ||||
|                     tagsSource.ping(); | ||||
|                 } catch (e) { | ||||
|                     console.warn(e) | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     ) | ||||
|     private static isOpen = new SimpleMetaTagger( | ||||
|         ["_isOpen", "_isOpen:description"], | ||||
|         "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')", | ||||
|         (feature => { | ||||
| 
 | ||||
|             const tagsSource = State.state.allElements.getEventSourceFor(feature); | ||||
|             tagsSource.addCallbackAndRun(tags => { | ||||
|                 if (tags.opening_hours === undefined || tags._country === undefined) { | ||||
|                     return; | ||||
|                 } | ||||
|                 try { | ||||
| 
 | ||||
|                     const oh = new opening_hours(tags["opening_hours"], { | ||||
|                         lat: tags._lat, | ||||
|                         lon: tags._lon, | ||||
|                         address: { | ||||
|                             country_code: tags._country.toLowerCase() | ||||
|                         } | ||||
|                     }, {tag_key: "opening_hours"}); | ||||
|                     // AUtomatically triggered on the next change
 | ||||
|                     const updateTags = () => { | ||||
|                         const oldValueIsOpen = tags["_isOpen"]; | ||||
|                         const oldNextChange = tags["_isOpen:nextTrigger"] ?? 0; | ||||
| 
 | ||||
|                         if (oldNextChange > (new Date()).getTime() && | ||||
|                             tags["_isOpen:oldvalue"] === tags["opening_hours"]) { | ||||
|                             // Already calculated and should not yet be triggered
 | ||||
|                             return; | ||||
|                         } | ||||
| 
 | ||||
|                         tags["_isOpen"] = oh.getState() ? "yes" : "no"; | ||||
|                         const comment = oh.getComment(); | ||||
|                         if (comment) { | ||||
|                             tags["_isOpen:description"] = comment; | ||||
|                         } | ||||
| 
 | ||||
|                         if (oldValueIsOpen !== tags._isOpen) { | ||||
|                             tagsSource.ping(); | ||||
|                         } | ||||
| 
 | ||||
|                         const nextChange = oh.getNextChange(); | ||||
|                         if (nextChange !== undefined) { | ||||
|                             const timeout = nextChange.getTime() - (new Date()).getTime(); | ||||
|                             tags["_isOpen:nextTrigger"] = nextChange.getTime(); | ||||
|                             tags["_isOpen:oldvalue"] = tags.opening_hours | ||||
|                             window.setTimeout( | ||||
|                                 () => { | ||||
|                                     console.log("Updating the _isOpen tag for ", tags.id, ", it's timer expired after", timeout); | ||||
|                                     updateTags(); | ||||
|                                 }, | ||||
|                                 timeout | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                     updateTags(); | ||||
|                 } catch (e) { | ||||
|                     console.warn("Error while parsing opening hours of ", tags.id, e); | ||||
|                     tags["_isOpen"] = "parse_error"; | ||||
|                 } | ||||
| 
 | ||||
|             }) | ||||
|         }) | ||||
|     ) | ||||
|     private static directionSimplified = new SimpleMetaTagger( | ||||
|         ["_direction:simplified", "_direction:leftright"], "_direction:simplified turns 'camera:direction' and 'direction' into either 0, 45, 90, 135, 180, 225, 270 or 315, whichever is closest. _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map", | ||||
|         (feature => { | ||||
|             const tags = feature.properties; | ||||
|             const direction = tags["camera:direction"] ?? tags["direction"]; | ||||
|             if (direction === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|             let n = Number(direction); | ||||
|             if (isNaN(n)) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // [22.5 -> 67.5] is sector 1
 | ||||
|             // [67.5 -> ] is sector 1
 | ||||
|             n = (n + 22.5) % 360; | ||||
|             n = Math.floor(n / 45); | ||||
|             tags["_direction:simplified"] = n; | ||||
|             tags["_direction:leftright"] = n <= 3 ? "right" : "left"; | ||||
| 
 | ||||
| 
 | ||||
|         }) | ||||
|     ) | ||||
|     private static carriageWayWidth = new SimpleMetaTagger( | ||||
|         ["_width:needed", "_width:needed:no_pedestrians", "_width:difference"], | ||||
|         "Legacy for a specific project calculating the needed width for safe traffic on a road. Only activated if 'width:carriageway' is present", | ||||
|         (feature: any, index: number) => { | ||||
| 
 | ||||
|             const properties = feature.properties; | ||||
|             if (properties["width:carriageway"] === undefined) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const carWidth = 2; | ||||
|             const cyclistWidth = 1.5; | ||||
|             const pedestrianWidth = 0.75; | ||||
| 
 | ||||
| 
 | ||||
|             const _leftSideParking = | ||||
|                 new And([new Tag("parking:lane:left", "parallel"), new Tag("parking:lane:right", "no_parking")]); | ||||
|             const _rightSideParking = | ||||
|                 new And([new Tag("parking:lane:right", "parallel"), new Tag("parking:lane:left", "no_parking")]); | ||||
| 
 | ||||
|             const _bothSideParking = new Tag("parking:lane:both", "parallel"); | ||||
|             const _noSideParking = new Tag("parking:lane:both", "no_parking"); | ||||
|             const _otherParkingMode = | ||||
|                 new Or([ | ||||
|                     new Tag("parking:lane:both", "perpendicular"), | ||||
|                     new Tag("parking:lane:left", "perpendicular"), | ||||
|                     new Tag("parking:lane:right", "perpendicular"), | ||||
|                     new Tag("parking:lane:both", "diagonal"), | ||||
|                     new Tag("parking:lane:left", "diagonal"), | ||||
|                     new Tag("parking:lane:right", "diagonal"), | ||||
|                 ]) | ||||
| 
 | ||||
|             const _sidewalkBoth = new Tag("sidewalk", "both"); | ||||
|             const _sidewalkLeft = new Tag("sidewalk", "left"); | ||||
|             const _sidewalkRight = new Tag("sidewalk", "right"); | ||||
|             const _sidewalkNone = new Tag("sidewalk", "none"); | ||||
| 
 | ||||
| 
 | ||||
|             let parallelParkingCount = 0; | ||||
| 
 | ||||
| 
 | ||||
|             const _oneSideParking = new Or([_leftSideParking, _rightSideParking]); | ||||
| 
 | ||||
|             if (_oneSideParking.matchesProperties(properties)) { | ||||
|                 parallelParkingCount = 1; | ||||
|             } else if (_bothSideParking.matchesProperties(properties)) { | ||||
|                 parallelParkingCount = 2; | ||||
|             } else if (_noSideParking.matchesProperties(properties)) { | ||||
|                 parallelParkingCount = 0; | ||||
|             } else if (_otherParkingMode.matchesProperties(properties)) { | ||||
|                 parallelParkingCount = 0; | ||||
|             } else { | ||||
|                 console.log("No parking data for ", properties.name, properties.id, properties) | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             let pedestrianFlowNeeded; | ||||
|             if (_sidewalkBoth.matchesProperties(properties)) { | ||||
|                 pedestrianFlowNeeded = 0; | ||||
|             } else if (_sidewalkNone.matchesProperties(properties)) { | ||||
|                 pedestrianFlowNeeded = 2; | ||||
|             } else if (_sidewalkLeft.matchesProperties(properties) || _sidewalkRight.matchesProperties(properties)) { | ||||
|                 pedestrianFlowNeeded = 1; | ||||
|             } else { | ||||
|                 pedestrianFlowNeeded = -1; | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             let onewayCar = properties.oneway === "yes"; | ||||
|             let onewayBike = properties["oneway:bicycle"] === "yes" || | ||||
|                 (onewayCar && properties["oneway:bicycle"] === undefined) | ||||
| 
 | ||||
|             let cyclingAllowed = | ||||
|                 !(properties.bicycle === "use_sidepath" | ||||
|                     || properties.bicycle === "no"); | ||||
| 
 | ||||
|             let carWidthUsed = (onewayCar ? 1 : 2) * carWidth; | ||||
|             properties["_width:needed:cars"] = Utils.Round(carWidthUsed); | ||||
|             properties["_width:needed:parking"] = Utils.Round(parallelParkingCount * carWidth) | ||||
| 
 | ||||
| 
 | ||||
|             let cyclistWidthUsed = 0; | ||||
|             if (cyclingAllowed) { | ||||
|                 cyclistWidthUsed = (onewayBike ? 1 : 2) * cyclistWidth; | ||||
|             } | ||||
|             properties["_width:needed:cyclists"] = Utils.Round(cyclistWidthUsed) | ||||
| 
 | ||||
| 
 | ||||
|             const width = parseFloat(properties["width:carriageway"]); | ||||
| 
 | ||||
| 
 | ||||
|             const targetWidthIgnoringPedestrians = | ||||
|                 carWidthUsed + | ||||
|                 cyclistWidthUsed + | ||||
|                 parallelParkingCount * carWidthUsed; | ||||
|             properties["_width:needed:no_pedestrians"] = Utils.Round(targetWidthIgnoringPedestrians); | ||||
| 
 | ||||
|             const pedestriansNeed = Math.max(0, pedestrianFlowNeeded) * pedestrianWidth; | ||||
|             const targetWidth = targetWidthIgnoringPedestrians + pedestriansNeed; | ||||
|             properties["_width:needed"] = Utils.Round(targetWidth); | ||||
|             properties["_width:needed:pedestrians"] = Utils.Round(pedestriansNeed) | ||||
| 
 | ||||
| 
 | ||||
|             properties["_width:difference"] = Utils.Round(targetWidth - width); | ||||
|             properties["_width:difference:no_pedestrians"] = Utils.Round(targetWidthIgnoringPedestrians - width); | ||||
| 
 | ||||
|         } | ||||
|     ); | ||||
|     private static currentTime = new SimpleMetaTagger( | ||||
|         ["_now:date", "_now:datetime", "_loaded:date", "_loaded:_datetime"], | ||||
|         "Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely", | ||||
|         (feature, _, freshness) => { | ||||
|             const now = new Date(); | ||||
| 
 | ||||
|             if (typeof freshness === "string") { | ||||
|                 freshness = new Date(freshness) | ||||
|             } | ||||
| 
 | ||||
|             function date(d: Date) { | ||||
|                 return d.toISOString().slice(0, 10); | ||||
|             } | ||||
| 
 | ||||
|             function datetime(d: Date) { | ||||
|                 return d.toISOString().slice(0, -5).replace("T", " "); | ||||
|             } | ||||
| 
 | ||||
|             feature.properties["_now:date"] = date(now); | ||||
|             feature.properties["_now:datetime"] = datetime(now); | ||||
|             feature.properties["_loaded:date"] = date(freshness); | ||||
|             feature.properties["_loaded:datetime"] = datetime(freshness); | ||||
| 
 | ||||
|         } | ||||
|     ) | ||||
|     private static metatags = [ | ||||
|         MetaTagging.latlon, | ||||
|         MetaTagging.surfaceArea, | ||||
|         MetaTagging.country, | ||||
|         MetaTagging.isOpen, | ||||
|         MetaTagging.carriageWayWidth, | ||||
|         MetaTagging.directionSimplified, | ||||
|         MetaTagging.currentTime | ||||
| 
 | ||||
|     ]; | ||||
| 
 | ||||
|     /** | ||||
|      * An actor which adds metatags on every feature in the given object | ||||
|      * The features are a list of geojson-features, with a "properties"-field and geometry | ||||
|      */ | ||||
|     static addMetatags(features: { feature: any, freshness: Date }[]) { | ||||
|     static addMetatags(features: { feature: any; freshness: Date }[], layers: LayerConfig[]) { | ||||
| 
 | ||||
|         for (const metatag of MetaTagging.metatags) { | ||||
|         for (const metatag of SimpleMetaTagger.metatags) { | ||||
|             try { | ||||
|                 metatag.addMetaTags(features); | ||||
|             } catch (e) { | ||||
|  | @ -327,32 +64,63 @@ export default class MetaTagging { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     static GetCountryCodeFor(lon: number, lat: number, callback: (country: string) => void) { | ||||
|         MetaTagging.coder.GetCountryCodeFor(lon, lat, callback) | ||||
|     } | ||||
| 
 | ||||
|     static HelpText(): UIElement { | ||||
|         const subElements: UIElement[] = [ | ||||
|             new Combine([ | ||||
|                 "<h1>Metatags</h1>", | ||||
|                 "Metatags are extra tags available, in order to display more data or to give better questions.", | ||||
|                 "The are calculated when the data arrives in the webbrowser. This document gives an overview of the available metatags" | ||||
|             ]) | ||||
|              | ||||
|              | ||||
|         ]; | ||||
| 
 | ||||
|         for (const metatag of MetaTagging.metatags) { | ||||
|             subElements.push( | ||||
|                 new Combine([ | ||||
|                     "<h3>", metatag.keys.join(", "), "</h3>", | ||||
|                     metatag.doc] | ||||
|                 ) | ||||
|             ) | ||||
|         // The functions - per layer - which add the new keys
 | ||||
|         const layerFuncs = new Map<string, ((feature: any) => void)>(); | ||||
|         for (const layer of layers) { | ||||
|             layerFuncs.set(layer.id, this.createRetaggingFunc(layer)); | ||||
|         } | ||||
| 
 | ||||
|         return new Combine(subElements) | ||||
|         for (const feature of features) { | ||||
|             // @ts-ignore
 | ||||
|             const key = feature.feature._matching_layer_id; | ||||
|             const f = layerFuncs.get(key); | ||||
|             if (f === undefined) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             f(feature.feature) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private static createRetaggingFunc(layer: LayerConfig): ((feature: any) => void) { | ||||
|         const calculatedTags: [string, string][] = layer.calculatedTags; | ||||
|         if (calculatedTags === undefined) { | ||||
|             return undefined; | ||||
|         } | ||||
| 
 | ||||
|         const functions: ((feature: any) => void)[] = []; | ||||
|         for (const entry of calculatedTags) { | ||||
|             const key = entry[0] | ||||
|             const code = entry[1]; | ||||
|             if (code === undefined) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             const func = new Function("feat", "return " + code + ";"); | ||||
| 
 | ||||
|             const f = (feature: any) => { | ||||
|                 feature.properties[key] = func(feature); | ||||
|             } | ||||
|             functions.push(f) | ||||
|         } | ||||
|         return (feature) => { | ||||
|             const tags = feature.properties | ||||
|             if (tags === undefined) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             ExtraFunction.FullPatchFeature(feature); | ||||
| 
 | ||||
|             for (const f of functions) { | ||||
|                 try { | ||||
|                     f(feature); | ||||
|                 } catch (e) { | ||||
|                     console.error("While calculating a tag value: ", e) | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										333
									
								
								Logic/SimpleMetaTagger.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										333
									
								
								Logic/SimpleMetaTagger.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,333 @@ | |||
| import {GeoOperations} from "./GeoOperations"; | ||||
| import State from "../State"; | ||||
| import {And} from "./And"; | ||||
| import {Tag} from "./Tag"; | ||||
| import {Or} from "./Or"; | ||||
| import {Utils} from "../Utils"; | ||||
| import opening_hours from "opening_hours"; | ||||
| import {UIElement} from "../UI/UIElement"; | ||||
| import Combine from "../UI/Base/Combine"; | ||||
| 
 | ||||
| export default class SimpleMetaTagger { | ||||
|     public readonly keys: string[]; | ||||
|     public readonly doc: string; | ||||
|     private readonly _f: (feature: any, index: number, freshness: Date) => void; | ||||
| 
 | ||||
|     constructor(keys: string[], doc: string, f: ((feature: any, index: number, freshness: Date) => void)) { | ||||
|         this.keys = keys; | ||||
|         this.doc = doc; | ||||
|         this._f = f; | ||||
|         for (const key of keys) { | ||||
|             if (!key.startsWith('_')) { | ||||
|                 throw `Incorrect metakey ${key}: it should start with underscore (_)` | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     addMetaTags(features: { feature: any, freshness: Date }[]) { | ||||
|         for (let i = 0; i < features.length; i++) { | ||||
|             let feature = features[i]; | ||||
|             this._f(feature.feature, i, feature.freshness); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static coder: any; | ||||
|     private static latlon = new SimpleMetaTagger(["_lat", "_lon"], "The latitude and longitude of the point (or centerpoint in the case of a way/area)", | ||||
|         (feature => { | ||||
|             const centerPoint = GeoOperations.centerpoint(feature); | ||||
|             const lat = centerPoint.geometry.coordinates[1]; | ||||
|             const lon = centerPoint.geometry.coordinates[0]; | ||||
|             feature.properties["_lat"] = "" + lat; | ||||
|             feature.properties["_lon"] = "" + lon; | ||||
|             feature._lon = lon; // This is dirty, I know
 | ||||
|             feature._lat = lat; | ||||
|         }) | ||||
|     ); | ||||
|     private static surfaceArea = new SimpleMetaTagger( | ||||
|         ["_surface", "_surface:ha"], "The surface area of the feature, in square meters and in hectare. Not set on points and ways", | ||||
|         (feature => { | ||||
|             const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature); | ||||
|             feature.properties["_surface"] = "" + sqMeters; | ||||
|             feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10; | ||||
| 
 | ||||
|         }) | ||||
|     ); | ||||
|     private static country = new SimpleMetaTagger( | ||||
|         ["_country"], "The country code of the property (with latlon2country)", | ||||
|         feature => { | ||||
| 
 | ||||
| 
 | ||||
|             let centerPoint: any = GeoOperations.centerpoint(feature); | ||||
|             const lat = centerPoint.geometry.coordinates[1]; | ||||
|             const lon = centerPoint.geometry.coordinates[0]; | ||||
| 
 | ||||
|             SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => { | ||||
|                 try { | ||||
|                     feature.properties["_country"] = countries[0].trim().toLowerCase(); | ||||
|                     const tagsSource = State.state.allElements.getEventSourceFor(feature); | ||||
|                     tagsSource.ping(); | ||||
|                 } catch (e) { | ||||
|                     console.warn(e) | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     ) | ||||
|     private static isOpen = new SimpleMetaTagger( | ||||
|         ["_isOpen", "_isOpen:description"], | ||||
|         "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')", | ||||
|         (feature => { | ||||
| 
 | ||||
|             const tagsSource = State.state.allElements.getEventSourceFor(feature); | ||||
|             tagsSource.addCallbackAndRun(tags => { | ||||
|                 if (tags.opening_hours === undefined || tags._country === undefined) { | ||||
|                     return; | ||||
|                 } | ||||
|                 try { | ||||
| 
 | ||||
|                     const oh = new opening_hours(tags["opening_hours"], { | ||||
|                         lat: tags._lat, | ||||
|                         lon: tags._lon, | ||||
|                         address: { | ||||
|                             country_code: tags._country.toLowerCase() | ||||
|                         } | ||||
|                     }, {tag_key: "opening_hours"}); | ||||
|                     // AUtomatically triggered on the next change
 | ||||
|                     const updateTags = () => { | ||||
|                         const oldValueIsOpen = tags["_isOpen"]; | ||||
|                         const oldNextChange = tags["_isOpen:nextTrigger"] ?? 0; | ||||
| 
 | ||||
|                         if (oldNextChange > (new Date()).getTime() && | ||||
|                             tags["_isOpen:oldvalue"] === tags["opening_hours"]) { | ||||
|                             // Already calculated and should not yet be triggered
 | ||||
|                             return; | ||||
|                         } | ||||
| 
 | ||||
|                         tags["_isOpen"] = oh.getState() ? "yes" : "no"; | ||||
|                         const comment = oh.getComment(); | ||||
|                         if (comment) { | ||||
|                             tags["_isOpen:description"] = comment; | ||||
|                         } | ||||
| 
 | ||||
|                         if (oldValueIsOpen !== tags._isOpen) { | ||||
|                             tagsSource.ping(); | ||||
|                         } | ||||
| 
 | ||||
|                         const nextChange = oh.getNextChange(); | ||||
|                         if (nextChange !== undefined) { | ||||
|                             const timeout = nextChange.getTime() - (new Date()).getTime(); | ||||
|                             tags["_isOpen:nextTrigger"] = nextChange.getTime(); | ||||
|                             tags["_isOpen:oldvalue"] = tags.opening_hours | ||||
|                             window.setTimeout( | ||||
|                                 () => { | ||||
|                                     console.log("Updating the _isOpen tag for ", tags.id, ", it's timer expired after", timeout); | ||||
|                                     updateTags(); | ||||
|                                 }, | ||||
|                                 timeout | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                     updateTags(); | ||||
|                 } catch (e) { | ||||
|                     console.warn("Error while parsing opening hours of ", tags.id, e); | ||||
|                     tags["_isOpen"] = "parse_error"; | ||||
|                 } | ||||
| 
 | ||||
|             }) | ||||
|         }) | ||||
|     ) | ||||
|     private static directionSimplified = new SimpleMetaTagger( | ||||
|         ["_direction:simplified", "_direction:leftright"], "_direction:simplified turns 'camera:direction' and 'direction' into either 0, 45, 90, 135, 180, 225, 270 or 315, whichever is closest. _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map", | ||||
|         (feature => { | ||||
|             const tags = feature.properties; | ||||
|             const direction = tags["camera:direction"] ?? tags["direction"]; | ||||
|             if (direction === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|             let n = Number(direction); | ||||
|             if (isNaN(n)) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // [22.5 -> 67.5] is sector 1
 | ||||
|             // [67.5 -> ] is sector 1
 | ||||
|             n = (n + 22.5) % 360; | ||||
|             n = Math.floor(n / 45); | ||||
|             tags["_direction:simplified"] = n; | ||||
|             tags["_direction:leftright"] = n <= 3 ? "right" : "left"; | ||||
| 
 | ||||
| 
 | ||||
|         }) | ||||
|     ) | ||||
|     private static carriageWayWidth = new SimpleMetaTagger( | ||||
|         ["_width:needed", "_width:needed:no_pedestrians", "_width:difference"], | ||||
|         "Legacy for a specific project calculating the needed width for safe traffic on a road. Only activated if 'width:carriageway' is present", | ||||
|         (feature: any, index: number) => { | ||||
| 
 | ||||
|             const properties = feature.properties; | ||||
|             if (properties["width:carriageway"] === undefined) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const carWidth = 2; | ||||
|             const cyclistWidth = 1.5; | ||||
|             const pedestrianWidth = 0.75; | ||||
| 
 | ||||
| 
 | ||||
|             const _leftSideParking = | ||||
|                 new And([new Tag("parking:lane:left", "parallel"), new Tag("parking:lane:right", "no_parking")]); | ||||
|             const _rightSideParking = | ||||
|                 new And([new Tag("parking:lane:right", "parallel"), new Tag("parking:lane:left", "no_parking")]); | ||||
| 
 | ||||
|             const _bothSideParking = new Tag("parking:lane:both", "parallel"); | ||||
|             const _noSideParking = new Tag("parking:lane:both", "no_parking"); | ||||
|             const _otherParkingMode = | ||||
|                 new Or([ | ||||
|                     new Tag("parking:lane:both", "perpendicular"), | ||||
|                     new Tag("parking:lane:left", "perpendicular"), | ||||
|                     new Tag("parking:lane:right", "perpendicular"), | ||||
|                     new Tag("parking:lane:both", "diagonal"), | ||||
|                     new Tag("parking:lane:left", "diagonal"), | ||||
|                     new Tag("parking:lane:right", "diagonal"), | ||||
|                 ]) | ||||
| 
 | ||||
|             const _sidewalkBoth = new Tag("sidewalk", "both"); | ||||
|             const _sidewalkLeft = new Tag("sidewalk", "left"); | ||||
|             const _sidewalkRight = new Tag("sidewalk", "right"); | ||||
|             const _sidewalkNone = new Tag("sidewalk", "none"); | ||||
| 
 | ||||
| 
 | ||||
|             let parallelParkingCount = 0; | ||||
| 
 | ||||
| 
 | ||||
|             const _oneSideParking = new Or([_leftSideParking, _rightSideParking]); | ||||
| 
 | ||||
|             if (_oneSideParking.matchesProperties(properties)) { | ||||
|                 parallelParkingCount = 1; | ||||
|             } else if (_bothSideParking.matchesProperties(properties)) { | ||||
|                 parallelParkingCount = 2; | ||||
|             } else if (_noSideParking.matchesProperties(properties)) { | ||||
|                 parallelParkingCount = 0; | ||||
|             } else if (_otherParkingMode.matchesProperties(properties)) { | ||||
|                 parallelParkingCount = 0; | ||||
|             } else { | ||||
|                 console.log("No parking data for ", properties.name, properties.id, properties) | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             let pedestrianFlowNeeded; | ||||
|             if (_sidewalkBoth.matchesProperties(properties)) { | ||||
|                 pedestrianFlowNeeded = 0; | ||||
|             } else if (_sidewalkNone.matchesProperties(properties)) { | ||||
|                 pedestrianFlowNeeded = 2; | ||||
|             } else if (_sidewalkLeft.matchesProperties(properties) || _sidewalkRight.matchesProperties(properties)) { | ||||
|                 pedestrianFlowNeeded = 1; | ||||
|             } else { | ||||
|                 pedestrianFlowNeeded = -1; | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             let onewayCar = properties.oneway === "yes"; | ||||
|             let onewayBike = properties["oneway:bicycle"] === "yes" || | ||||
|                 (onewayCar && properties["oneway:bicycle"] === undefined) | ||||
| 
 | ||||
|             let cyclingAllowed = | ||||
|                 !(properties.bicycle === "use_sidepath" | ||||
|                     || properties.bicycle === "no"); | ||||
| 
 | ||||
|             let carWidthUsed = (onewayCar ? 1 : 2) * carWidth; | ||||
|             properties["_width:needed:cars"] = Utils.Round(carWidthUsed); | ||||
|             properties["_width:needed:parking"] = Utils.Round(parallelParkingCount * carWidth) | ||||
| 
 | ||||
| 
 | ||||
|             let cyclistWidthUsed = 0; | ||||
|             if (cyclingAllowed) { | ||||
|                 cyclistWidthUsed = (onewayBike ? 1 : 2) * cyclistWidth; | ||||
|             } | ||||
|             properties["_width:needed:cyclists"] = Utils.Round(cyclistWidthUsed) | ||||
| 
 | ||||
| 
 | ||||
|             const width = parseFloat(properties["width:carriageway"]); | ||||
| 
 | ||||
| 
 | ||||
|             const targetWidthIgnoringPedestrians = | ||||
|                 carWidthUsed + | ||||
|                 cyclistWidthUsed + | ||||
|                 parallelParkingCount * carWidthUsed; | ||||
|             properties["_width:needed:no_pedestrians"] = Utils.Round(targetWidthIgnoringPedestrians); | ||||
| 
 | ||||
|             const pedestriansNeed = Math.max(0, pedestrianFlowNeeded) * pedestrianWidth; | ||||
|             const targetWidth = targetWidthIgnoringPedestrians + pedestriansNeed; | ||||
|             properties["_width:needed"] = Utils.Round(targetWidth); | ||||
|             properties["_width:needed:pedestrians"] = Utils.Round(pedestriansNeed) | ||||
| 
 | ||||
| 
 | ||||
|             properties["_width:difference"] = Utils.Round(targetWidth - width); | ||||
|             properties["_width:difference:no_pedestrians"] = Utils.Round(targetWidthIgnoringPedestrians - width); | ||||
| 
 | ||||
|         } | ||||
|     ); | ||||
|     private static currentTime = new SimpleMetaTagger( | ||||
|         ["_now:date", "_now:datetime", "_loaded:date", "_loaded:_datetime"], | ||||
|         "Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely", | ||||
|         (feature, _, freshness) => { | ||||
|             const now = new Date(); | ||||
| 
 | ||||
|             if (typeof freshness === "string") { | ||||
|                 freshness = new Date(freshness) | ||||
|             } | ||||
| 
 | ||||
|             function date(d: Date) { | ||||
|                 return d.toISOString().slice(0, 10); | ||||
|             } | ||||
| 
 | ||||
|             function datetime(d: Date) { | ||||
|                 return d.toISOString().slice(0, -5).replace("T", " "); | ||||
|             } | ||||
| 
 | ||||
|             feature.properties["_now:date"] = date(now); | ||||
|             feature.properties["_now:datetime"] = datetime(now); | ||||
|             feature.properties["_loaded:date"] = date(freshness); | ||||
|             feature.properties["_loaded:datetime"] = datetime(freshness); | ||||
| 
 | ||||
|         } | ||||
|     ) | ||||
|     public static metatags = [ | ||||
|         SimpleMetaTagger.latlon, | ||||
|         SimpleMetaTagger.surfaceArea, | ||||
|         SimpleMetaTagger.country, | ||||
|         SimpleMetaTagger.isOpen, | ||||
|         SimpleMetaTagger.carriageWayWidth, | ||||
|         SimpleMetaTagger.directionSimplified, | ||||
|         SimpleMetaTagger.currentTime | ||||
| 
 | ||||
|     ]; | ||||
| 
 | ||||
|     static GetCountryCodeFor(lon: number, lat: number, callback: (country: string) => void) { | ||||
|         SimpleMetaTagger.coder.GetCountryCodeFor(lon, lat, callback) | ||||
|     } | ||||
| 
 | ||||
|     static HelpText(): UIElement { | ||||
|         const subElements: UIElement[] = [ | ||||
|             new Combine([ | ||||
|                 "<h1>Metatags</h1>", | ||||
|                 "Metatags are extra tags available, in order to display more data or to give better questions.", | ||||
|                 "The are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags" | ||||
|             ]) | ||||
| 
 | ||||
| 
 | ||||
|         ]; | ||||
| 
 | ||||
|         for (const metatag of SimpleMetaTagger.metatags) { | ||||
|             subElements.push( | ||||
|                 new Combine([ | ||||
|                     "<h3>", metatag.keys.join(", "), "</h3>", | ||||
|                     metatag.doc] | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         return new Combine(subElements) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -2,7 +2,7 @@ import { Utils } from "../Utils"; | |||
| 
 | ||||
| export default class Constants { | ||||
|      | ||||
|     public static vNumber = "0.6.0a"; | ||||
|     public static vNumber = "0.6.1"; | ||||
| 
 | ||||
|     // The user journey states thresholds when a new feature gets unlocked
 | ||||
|     public static userJourney = { | ||||
|  | @ -12,7 +12,7 @@ export default class Constants { | |||
|         tagsVisibleAt: 25, | ||||
|         mapCompleteHelpUnlock: 50, | ||||
|         tagsVisibleAndWikiLinked: 30, | ||||
|         themeGeneratorReadOnlyUnlock: 100, | ||||
|         themeGeneratorReadOnlyUnlock: 50, | ||||
|         themeGeneratorFullUnlock: 500, | ||||
|         addNewPointWithUnreadMessagesUnlock: 500, | ||||
|         minZoomLevelToAddNewPoints: (Constants.isRetina() ? 18 : 19) | ||||
|  |  | |||
|  | @ -5,52 +5,54 @@ import Combine from "../Base/Combine"; | |||
| import LanguagePicker from "../LanguagePicker"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| 
 | ||||
| export default class ThemeIntroductionPanel extends UIElement { | ||||
|     private languagePicker: UIElement; | ||||
| 
 | ||||
|     private readonly description: UIElement; | ||||
|     private readonly plzLogIn: UIElement; | ||||
|     private readonly welcomeBack: UIElement; | ||||
|     private readonly tail: UIElement; | ||||
|     private readonly loginStatus: UIElement; | ||||
|     private _layout: UIEventSource<LayoutConfig>; | ||||
| 
 | ||||
| 
 | ||||
|     constructor() { | ||||
|         super(State.state.osmConnection.userDetails); | ||||
|         this.ListenTo(Locale.language); | ||||
|         this.languagePicker = LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language, Translations.t.general.pickLanguage); | ||||
|         const layout = State.state.layoutToUse.data; | ||||
|         this._layout = State.state.layoutToUse; | ||||
|         this.ListenTo(State.state.layoutToUse); | ||||
| 
 | ||||
|         this.description = layout.description | ||||
|         this.plzLogIn = | ||||
|         const plzLogIn = | ||||
|             Translations.t.general.loginWithOpenStreetMap | ||||
|                 .onClick(() => { | ||||
|                     State.state.osmConnection.AttemptLogin() | ||||
|                 }); | ||||
|         this.welcomeBack = Translations.t.general.welcomeBack; | ||||
|         this.tail = layout.descriptionTail; | ||||
|          | ||||
|          | ||||
|         const welcomeBack = Translations.t.general.welcomeBack; | ||||
|          | ||||
|         this.loginStatus = new VariableUiElement( | ||||
|             State.state.osmConnection.userDetails.map( | ||||
|                 userdetails => { | ||||
|                     if (State.state.featureSwitchUserbadge.data) { | ||||
|                         return ""; | ||||
|                     } | ||||
|                     return (userdetails.loggedIn ? this.welcomeBack : this.plzLogIn).Render(); | ||||
|                     return (userdetails.loggedIn ? welcomeBack : plzLogIn).Render(); | ||||
|                 } | ||||
|             ) | ||||
|              | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|         const layout : LayoutConfig = this._layout.data; | ||||
|         return new Combine([ | ||||
|             this.description, | ||||
|             layout.description, | ||||
|             "<br/><br/>", | ||||
|             this.loginStatus, | ||||
|             this.tail, | ||||
|             layout.descriptionTail, | ||||
|             "<br/>", | ||||
|             this.languagePicker | ||||
|             this.languagePicker, | ||||
|             ...layout.CustomCodeSnippets() | ||||
|         ]).Render() | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -15,6 +15,9 @@ | |||
|   "source": { | ||||
|     "osmTags": "amenity=public_bookcase" | ||||
|   }, | ||||
|   "calculatedTags": { | ||||
|     "_distanceToPietervdn": "feat.distanceTo(3.704388, 51.05281) < 1 ? 'closeby' : 'faraway'" | ||||
|   }, | ||||
|   "minzoom": 12, | ||||
|   "wayHandling": 2, | ||||
|   "title": { | ||||
|  |  | |||
|  | @ -50,7 +50,7 @@ | |||
|           { | ||||
|             "if": { | ||||
|               "and": [ | ||||
|                 "name:nl~" | ||||
|                 "name:nl~*" | ||||
|               ] | ||||
|             }, | ||||
|             "then": { | ||||
|  |  | |||
							
								
								
									
										6
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										6
									
								
								index.ts
									
										
									
									
									
								
							|  | @ -14,10 +14,10 @@ import Translations from "./UI/i18n/Translations"; | |||
| 
 | ||||
| import CountryCoder from "latlon2country" | ||||
| 
 | ||||
| import MetaTagging from "./Logic/MetaTagging"; | ||||
| import SimpleMetaTagger from "./Logic/SimpleMetaTagger"; | ||||
| 
 | ||||
| // Workaround for a stupid crash: inject the function
 | ||||
| MetaTagging.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); | ||||
| SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); | ||||
| 
 | ||||
| 
 | ||||
| let defaultLayout = "" | ||||
|  | @ -87,7 +87,7 @@ if (layoutFromBase64.startsWith("http")) { | |||
|                 const parsed = JSON.parse(data); | ||||
|                 // Overwrite the id to the wiki:value
 | ||||
|                 parsed.id = link; | ||||
|                 const layout = new LayoutConfig(parsed); | ||||
|                 const layout = new LayoutConfig(parsed, false); | ||||
|                 InitUiElements.InitAll(layout, layoutFromBase64, testing, layoutFromBase64, btoa(data)); | ||||
|             } catch (e) { | ||||
|                 new FixedUiElement(`<a href="${link}">${link}</a> is invalid:<br/>${e}<br/> <a href='https://${window.location.host}/'>Go back</a>")`) | ||||
|  |  | |||
							
								
								
									
										14520
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										14520
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -32,6 +32,7 @@ | |||
|   "dependencies": { | ||||
|     "@babel/preset-env": "7.13.8", | ||||
|     "@tailwindcss/postcss7-compat": "^2.0.2", | ||||
|     "@turf/distance": "^6.3.0", | ||||
|     "@types/jquery": "^3.5.5", | ||||
|     "@types/leaflet-markercluster": "^1.0.3", | ||||
|     "@types/leaflet-providers": "^1.2.0", | ||||
|  |  | |||
|  | @ -1,9 +1,10 @@ | |||
| import {Utils} from "../Utils"; | ||||
| Utils.runningFromConsole = true; | ||||
| import SpecialVisualizations from "../UI/SpecialVisualizations"; | ||||
| import {existsSync, mkdirSync, readFileSync, writeFile, writeFileSync} from "fs"; | ||||
| import {writeFileSync} from "fs"; | ||||
| import {UIElement} from "../UI/UIElement"; | ||||
| import MetaTagging from "../Logic/MetaTagging"; | ||||
| import SimpleMetaTagger from "../Logic/SimpleMetaTagger"; | ||||
| 
 | ||||
| Utils.runningFromConsole = true; | ||||
| 
 | ||||
| 
 | ||||
| const TurndownService = require('turndown') | ||||
|  | @ -14,7 +15,7 @@ function WriteFile(filename, html: UIElement) : void { | |||
| } | ||||
| 
 | ||||
| WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage) | ||||
| WriteFile("./Docs/CalculatedTags.md", MetaTagging.HelpText()) | ||||
| WriteFile("./Docs/CalculatedTags.md", SimpleMetaTagger.HelpText()) | ||||
| 
 | ||||
| 
 | ||||
| console.log("Generated docs") | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue