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 |     name: Translation | ||||||
|     description: Translation; |     description: Translation; | ||||||
|     source: SourceConfig; |     source: SourceConfig; | ||||||
|  |     calculatedTags: [string, string][] | ||||||
|     doNotDownload: boolean; |     doNotDownload: boolean; | ||||||
|     passAllFeatures: boolean; |     passAllFeatures: boolean; | ||||||
|     minzoom: number; |     minzoom: number; | ||||||
|  | @ -53,6 +54,7 @@ export default class LayerConfig { | ||||||
|     tagRenderings: TagRenderingConfig []; |     tagRenderings: TagRenderingConfig []; | ||||||
| 
 | 
 | ||||||
|     constructor(json: LayerConfigJson, |     constructor(json: LayerConfigJson, | ||||||
|  |                 official: boolean= true, | ||||||
|                 context?: string) { |                 context?: string) { | ||||||
|         context = context + "." + json.id; |         context = context + "." + json.id; | ||||||
|         const self = this; |         const self = this; | ||||||
|  | @ -88,7 +90,14 @@ export default class LayerConfig { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |         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.doNotDownload = json.doNotDownload ?? false; | ||||||
|         this.passAllFeatures = json.passAllFeatures ?? false; |         this.passAllFeatures = json.passAllFeatures ?? false; | ||||||
|  | @ -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: { |     public AddRoamingRenderings(addAll: { | ||||||
|         tagRenderings: TagRenderingConfig[], |         tagRenderings: TagRenderingConfig[], | ||||||
|  |  | ||||||
|  | @ -41,6 +41,11 @@ export interface LayerConfigJson { | ||||||
|      */ |      */ | ||||||
|     source: {osmTags: AndOrTagConfigJson | string} | {geoJsonSource: string} | {overpassScript: string} |     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.  |      * 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 |      * Works well together with 'passAllFeatures', to add decoration | ||||||
|  |  | ||||||
|  | @ -39,10 +39,12 @@ export default class LayoutConfig { | ||||||
|     public readonly enableLayers: boolean; |     public readonly enableLayers: boolean; | ||||||
|     public readonly enableSearch: boolean; |     public readonly enableSearch: boolean; | ||||||
|     public readonly enableGeolocation: boolean; |     public readonly enableGeolocation: boolean; | ||||||
|  |     private readonly _official : boolean; | ||||||
|     public readonly enableBackgroundLayerSelection: boolean; |     public readonly enableBackgroundLayerSelection: boolean; | ||||||
|     public readonly customCss?: string; |     public readonly customCss?: string; | ||||||
| 
 | 
 | ||||||
|     constructor(json: LayoutConfigJson, context?: string) { |     constructor(json: LayoutConfigJson, official=true, context?: string) { | ||||||
|  |         this._official = official; | ||||||
|         this.id = json.id; |         this.id = json.id; | ||||||
|         context = (context ?? "") + "." + this.id; |         context = (context ?? "") + "." + this.id; | ||||||
|         this.maintainer = json.maintainer; |         this.maintainer = json.maintainer; | ||||||
|  | @ -104,7 +106,7 @@ export default class LayoutConfig { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // @ts-ignore
 |             // @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!
 |         // ALl the layers are constructed, let them share tags in now!
 | ||||||
|  | @ -129,7 +131,6 @@ export default class LayoutConfig { | ||||||
|                     iconOverlays: [], |                     iconOverlays: [], | ||||||
|                     tagRenderings: this.roamingRenderings |                     tagRenderings: this.roamingRenderings | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -166,4 +167,19 @@ export default class LayoutConfig { | ||||||
|         this.customCss = json.customCss; |         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); |                 hashFromLocalStorage.setData(hash); | ||||||
|                 dedicatedHashFromLocalStorage.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); |             userLayoutParam.setData(layoutToUse.id); | ||||||
|             return layoutToUse; |             return layoutToUse; | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|  | @ -380,7 +380,6 @@ export class InitUiElements { | ||||||
|                 return flayers; |                 return flayers; | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap); |         const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap); | ||||||
|         State.state.layerUpdater = updater; |         State.state.layerUpdater = updater; | ||||||
|         const source = new FeaturePipeline(state.filteredLayers, updater, state.layoutToUse, state.changes, state.locationControl); |         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, |         new ShowDataLayer(source.features, State.state.leafletMap, | ||||||
|  |  | ||||||
|  | @ -24,7 +24,7 @@ export default class InstalledThemes { | ||||||
|                     try { |                     try { | ||||||
|                         const json = atob(customLayout.data); |                         const json = atob(customLayout.data); | ||||||
|                         const layout = new LayoutConfig( |                         const layout = new LayoutConfig( | ||||||
|                             JSON.parse(json)); |                             JSON.parse(json), false); | ||||||
|                         installedThemes.push({ |                         installedThemes.push({ | ||||||
|                             layout: layout, |                             layout: layout, | ||||||
|                             definition: customLayout.data |                             definition: customLayout.data | ||||||
|  |  | ||||||
|  | @ -21,6 +21,15 @@ export class GeoOperations { | ||||||
|         return coordinates; |         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, |     static featureIsContainedInAny(feature: any, | ||||||
|                                    shouldNotContain: any[], |                                    shouldNotContain: any[], | ||||||
|                                    maxOverlapPercentage: number): boolean { |                                    maxOverlapPercentage: number): boolean { | ||||||
|  |  | ||||||
|  | @ -1,39 +1,45 @@ | ||||||
| import {GeoOperations} from "./GeoOperations"; | import {GeoOperations} from "./GeoOperations"; | ||||||
| import State from "../State"; | import LayerConfig from "../Customizations/JSON/LayerConfig"; | ||||||
| import opening_hours from "opening_hours"; | import SimpleMetaTagger from "./SimpleMetaTagger"; | ||||||
| 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"; |  | ||||||
| 
 | 
 | ||||||
| class SimpleMetaTagger { | export class ExtraFunction { | ||||||
|     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)) { |     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." | ||||||
|         this.keys = keys; |     private static DistanceToFunc = new ExtraFunction( | ||||||
|         this.doc = doc; |         "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; |         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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     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, ... |  * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... | ||||||
|  | @ -43,282 +49,13 @@ class SimpleMetaTagger { | ||||||
| export default class MetaTagging { | 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 |      * 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 |      * 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 { |             try { | ||||||
|                 metatag.addMetaTags(features); |                 metatag.addMetaTags(features); | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|  | @ -327,32 +64,63 @@ export default class MetaTagging { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         // 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)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     static GetCountryCodeFor(lon: number, lat: number, callback: (country: string) => void) { |         for (const feature of features) { | ||||||
|         MetaTagging.coder.GetCountryCodeFor(lon, lat, callback) |             // @ts-ignore
 | ||||||
|  |             const key = feature.feature._matching_layer_id; | ||||||
|  |             const f = layerFuncs.get(key); | ||||||
|  |             if (f === undefined) { | ||||||
|  |                 continue; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|     static HelpText(): UIElement { |             f(feature.feature) | ||||||
|         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" | 
 | ||||||
|             ]) |     private static createRetaggingFunc(layer: LayerConfig): ((feature: any) => void) { | ||||||
|              |         const calculatedTags: [string, string][] = layer.calculatedTags; | ||||||
|              |         if (calculatedTags === undefined) { | ||||||
|         ]; |             return undefined; | ||||||
| 
 |         } | ||||||
|         for (const metatag of MetaTagging.metatags) { | 
 | ||||||
|             subElements.push( |         const functions: ((feature: any) => void)[] = []; | ||||||
|                 new Combine([ |         for (const entry of calculatedTags) { | ||||||
|                     "<h3>", metatag.keys.join(", "), "</h3>", |             const key = entry[0] | ||||||
|                     metatag.doc] |             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) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|         return new Combine(subElements) |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										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 { | 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
 |     // The user journey states thresholds when a new feature gets unlocked
 | ||||||
|     public static userJourney = { |     public static userJourney = { | ||||||
|  | @ -12,7 +12,7 @@ export default class Constants { | ||||||
|         tagsVisibleAt: 25, |         tagsVisibleAt: 25, | ||||||
|         mapCompleteHelpUnlock: 50, |         mapCompleteHelpUnlock: 50, | ||||||
|         tagsVisibleAndWikiLinked: 30, |         tagsVisibleAndWikiLinked: 30, | ||||||
|         themeGeneratorReadOnlyUnlock: 100, |         themeGeneratorReadOnlyUnlock: 50, | ||||||
|         themeGeneratorFullUnlock: 500, |         themeGeneratorFullUnlock: 500, | ||||||
|         addNewPointWithUnreadMessagesUnlock: 500, |         addNewPointWithUnreadMessagesUnlock: 500, | ||||||
|         minZoomLevelToAddNewPoints: (Constants.isRetina() ? 18 : 19) |         minZoomLevelToAddNewPoints: (Constants.isRetina() ? 18 : 19) | ||||||
|  |  | ||||||
|  | @ -5,52 +5,54 @@ import Combine from "../Base/Combine"; | ||||||
| import LanguagePicker from "../LanguagePicker"; | import LanguagePicker from "../LanguagePicker"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
|  | import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||||
|  | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| 
 | 
 | ||||||
| export default class ThemeIntroductionPanel extends UIElement { | export default class ThemeIntroductionPanel extends UIElement { | ||||||
|     private languagePicker: 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 readonly loginStatus: UIElement; | ||||||
|  |     private _layout: UIEventSource<LayoutConfig>; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(State.state.osmConnection.userDetails); |         super(State.state.osmConnection.userDetails); | ||||||
|         this.ListenTo(Locale.language); |         this.ListenTo(Locale.language); | ||||||
|         this.languagePicker = LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language, Translations.t.general.pickLanguage); |         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 |         const plzLogIn = | ||||||
|         this.plzLogIn = |  | ||||||
|             Translations.t.general.loginWithOpenStreetMap |             Translations.t.general.loginWithOpenStreetMap | ||||||
|                 .onClick(() => { |                 .onClick(() => { | ||||||
|                     State.state.osmConnection.AttemptLogin() |                     State.state.osmConnection.AttemptLogin() | ||||||
|                 }); |                 }); | ||||||
|         this.welcomeBack = Translations.t.general.welcomeBack; |          | ||||||
|         this.tail = layout.descriptionTail; |          | ||||||
|  |         const welcomeBack = Translations.t.general.welcomeBack; | ||||||
|  |          | ||||||
|         this.loginStatus = new VariableUiElement( |         this.loginStatus = new VariableUiElement( | ||||||
|             State.state.osmConnection.userDetails.map( |             State.state.osmConnection.userDetails.map( | ||||||
|                 userdetails => { |                 userdetails => { | ||||||
|                     if (State.state.featureSwitchUserbadge.data) { |                     if (State.state.featureSwitchUserbadge.data) { | ||||||
|                         return ""; |                         return ""; | ||||||
|                     } |                     } | ||||||
|                     return (userdetails.loggedIn ? this.welcomeBack : this.plzLogIn).Render(); |                     return (userdetails.loggedIn ? welcomeBack : plzLogIn).Render(); | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|              |  | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     InnerRender(): string { |     InnerRender(): string { | ||||||
|  |         const layout : LayoutConfig = this._layout.data; | ||||||
|         return new Combine([ |         return new Combine([ | ||||||
|             this.description, |             layout.description, | ||||||
|             "<br/><br/>", |             "<br/><br/>", | ||||||
|             this.loginStatus, |             this.loginStatus, | ||||||
|             this.tail, |             layout.descriptionTail, | ||||||
|             "<br/>", |             "<br/>", | ||||||
|             this.languagePicker |             this.languagePicker, | ||||||
|  |             ...layout.CustomCodeSnippets() | ||||||
|         ]).Render() |         ]).Render() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -15,6 +15,9 @@ | ||||||
|   "source": { |   "source": { | ||||||
|     "osmTags": "amenity=public_bookcase" |     "osmTags": "amenity=public_bookcase" | ||||||
|   }, |   }, | ||||||
|  |   "calculatedTags": { | ||||||
|  |     "_distanceToPietervdn": "feat.distanceTo(3.704388, 51.05281) < 1 ? 'closeby' : 'faraway'" | ||||||
|  |   }, | ||||||
|   "minzoom": 12, |   "minzoom": 12, | ||||||
|   "wayHandling": 2, |   "wayHandling": 2, | ||||||
|   "title": { |   "title": { | ||||||
|  |  | ||||||
|  | @ -50,7 +50,7 @@ | ||||||
|           { |           { | ||||||
|             "if": { |             "if": { | ||||||
|               "and": [ |               "and": [ | ||||||
|                 "name:nl~" |                 "name:nl~*" | ||||||
|               ] |               ] | ||||||
|             }, |             }, | ||||||
|             "then": { |             "then": { | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										6
									
								
								index.ts
									
										
									
									
									
								
							|  | @ -14,10 +14,10 @@ import Translations from "./UI/i18n/Translations"; | ||||||
| 
 | 
 | ||||||
| import CountryCoder from "latlon2country" | import CountryCoder from "latlon2country" | ||||||
| 
 | 
 | ||||||
| import MetaTagging from "./Logic/MetaTagging"; | import SimpleMetaTagger from "./Logic/SimpleMetaTagger"; | ||||||
| 
 | 
 | ||||||
| // Workaround for a stupid crash: inject the function
 | // 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 = "" | let defaultLayout = "" | ||||||
|  | @ -87,7 +87,7 @@ if (layoutFromBase64.startsWith("http")) { | ||||||
|                 const parsed = JSON.parse(data); |                 const parsed = JSON.parse(data); | ||||||
|                 // Overwrite the id to the wiki:value
 |                 // Overwrite the id to the wiki:value
 | ||||||
|                 parsed.id = link; |                 parsed.id = link; | ||||||
|                 const layout = new LayoutConfig(parsed); |                 const layout = new LayoutConfig(parsed, false); | ||||||
|                 InitUiElements.InitAll(layout, layoutFromBase64, testing, layoutFromBase64, btoa(data)); |                 InitUiElements.InitAll(layout, layoutFromBase64, testing, layoutFromBase64, btoa(data)); | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 new FixedUiElement(`<a href="${link}">${link}</a> is invalid:<br/>${e}<br/> <a href='https://${window.location.host}/'>Go back</a>")`) |                 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": { |   "dependencies": { | ||||||
|     "@babel/preset-env": "7.13.8", |     "@babel/preset-env": "7.13.8", | ||||||
|     "@tailwindcss/postcss7-compat": "^2.0.2", |     "@tailwindcss/postcss7-compat": "^2.0.2", | ||||||
|  |     "@turf/distance": "^6.3.0", | ||||||
|     "@types/jquery": "^3.5.5", |     "@types/jquery": "^3.5.5", | ||||||
|     "@types/leaflet-markercluster": "^1.0.3", |     "@types/leaflet-markercluster": "^1.0.3", | ||||||
|     "@types/leaflet-providers": "^1.2.0", |     "@types/leaflet-providers": "^1.2.0", | ||||||
|  |  | ||||||
|  | @ -1,9 +1,10 @@ | ||||||
| import {Utils} from "../Utils"; | import {Utils} from "../Utils"; | ||||||
| Utils.runningFromConsole = true; |  | ||||||
| import SpecialVisualizations from "../UI/SpecialVisualizations"; | import SpecialVisualizations from "../UI/SpecialVisualizations"; | ||||||
| import {existsSync, mkdirSync, readFileSync, writeFile, writeFileSync} from "fs"; | import {writeFileSync} from "fs"; | ||||||
| import {UIElement} from "../UI/UIElement"; | import {UIElement} from "../UI/UIElement"; | ||||||
| import MetaTagging from "../Logic/MetaTagging"; | import SimpleMetaTagger from "../Logic/SimpleMetaTagger"; | ||||||
|  | 
 | ||||||
|  | Utils.runningFromConsole = true; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| const TurndownService = require('turndown') | const TurndownService = require('turndown') | ||||||
|  | @ -14,7 +15,7 @@ function WriteFile(filename, html: UIElement) : void { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage) | WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage) | ||||||
| WriteFile("./Docs/CalculatedTags.md", MetaTagging.HelpText()) | WriteFile("./Docs/CalculatedTags.md", SimpleMetaTagger.HelpText()) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| console.log("Generated docs") | console.log("Generated docs") | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue