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;
|
||||||
|
@ -65,9 +67,9 @@ export default class LayerConfig {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
legacy = FromJSON.Tag(json["overpassTags"], context + ".overpasstags");
|
legacy = FromJSON.Tag(json["overpassTags"], context + ".overpasstags");
|
||||||
}
|
}
|
||||||
if(json.source !== undefined){
|
if (json.source !== undefined) {
|
||||||
if (legacy !== undefined ) {
|
if (legacy !== undefined) {
|
||||||
throw context+"Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined"
|
throw context + "Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined"
|
||||||
}
|
}
|
||||||
|
|
||||||
let osmTags: TagsFilter = legacy;
|
let osmTags: TagsFilter = legacy;
|
||||||
|
@ -81,14 +83,21 @@ export default class LayerConfig {
|
||||||
geojsonSource: json.source["geoJsonSource"],
|
geojsonSource: json.source["geoJsonSource"],
|
||||||
overpassScript: json.source["overpassScript"],
|
overpassScript: json.source["overpassScript"],
|
||||||
});
|
});
|
||||||
}else{
|
} else {
|
||||||
this.source = new SourceConfig({
|
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.doNotDownload = json.doNotDownload ?? false;
|
||||||
this.passAllFeatures = json.passAllFeatures ?? false;
|
this.passAllFeatures = json.passAllFeatures ?? false;
|
||||||
|
@ -139,7 +148,7 @@ export default class LayerConfig {
|
||||||
if (typeof renderingJson === "string") {
|
if (typeof renderingJson === "string") {
|
||||||
|
|
||||||
if (renderingJson === "questions") {
|
if (renderingJson === "questions") {
|
||||||
if(readOnly){
|
if (readOnly) {
|
||||||
throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}`
|
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}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default class LayoutConfig {
|
||||||
};
|
};
|
||||||
|
|
||||||
public readonly hideFromOverview: boolean;
|
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 enableUserBadge: boolean;
|
||||||
public readonly enableShareScreen: boolean;
|
public readonly enableShareScreen: boolean;
|
||||||
public readonly enableMoreQuests: boolean;
|
public readonly enableMoreQuests: boolean;
|
||||||
|
@ -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;
|
||||||
|
@ -54,7 +56,7 @@ export default class LayoutConfig {
|
||||||
} else {
|
} else {
|
||||||
this.language = json.language;
|
this.language = json.language;
|
||||||
}
|
}
|
||||||
if(this.language.length == 0){
|
if (this.language.length == 0) {
|
||||||
throw "No languages defined. Define at least one language"
|
throw "No languages defined. Define at least one language"
|
||||||
}
|
}
|
||||||
if (json.title === undefined) {
|
if (json.title === undefined) {
|
||||||
|
@ -66,7 +68,7 @@ export default class LayoutConfig {
|
||||||
this.title = new Translation(json.title, context + ".title");
|
this.title = new Translation(json.title, context + ".title");
|
||||||
this.description = new Translation(json.description, context + ".description");
|
this.description = new Translation(json.description, context + ".description");
|
||||||
this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, context + ".shortdescription");
|
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.icon = json.icon;
|
||||||
this.socialImage = json.socialImage;
|
this.socialImage = json.socialImage;
|
||||||
this.startZoom = json.startZoom;
|
this.startZoom = json.startZoom;
|
||||||
|
@ -79,7 +81,7 @@ export default class LayoutConfig {
|
||||||
return SharedTagRenderings.SharedTagRendering[tr];
|
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;
|
this.defaultBackgroundId = json.defaultBackgroundId;
|
||||||
|
@ -104,32 +106,31 @@ 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!
|
||||||
const roaming : {r, source: LayerConfig}[] = []
|
const roaming: { r, source: LayerConfig }[] = []
|
||||||
for (const layer of this.layers) {
|
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 layer of this.layers) {
|
||||||
for (const r of roaming) {
|
for (const r of roaming) {
|
||||||
if(r.source == layer){
|
if (r.source == layer) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
layer.AddRoamingRenderings(r.r);
|
layer.AddRoamingRenderings(r.r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const layer of this.layers) {
|
for (const layer of this.layers) {
|
||||||
layer.AddRoamingRenderings(
|
layer.AddRoamingRenderings(
|
||||||
{
|
{
|
||||||
titleIcons:[],
|
titleIcons: [],
|
||||||
iconOverlays: [],
|
iconOverlays: [],
|
||||||
tagRenderings: this.roamingRenderings
|
tagRenderings: this.roamingRenderings
|
||||||
}
|
}
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,8 +152,8 @@ export default class LayoutConfig {
|
||||||
|
|
||||||
this.hideFromOverview = json.hideFromOverview ?? false;
|
this.hideFromOverview = json.hideFromOverview ?? false;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if(json.hideInOverview){
|
if (json.hideInOverview) {
|
||||||
throw "The json for "+this.id+" contains a 'hideInOverview'. Did you mean hideFromOverview instead?"
|
throw "The json for " + this.id + " contains a 'hideInOverview'. Did you mean hideFromOverview instead?"
|
||||||
}
|
}
|
||||||
this.lockLocation = json.lockLocation ?? false;
|
this.lockLocation = json.lockLocation ?? false;
|
||||||
this.enableUserBadge = json.enableUserBadge ?? true;
|
this.enableUserBadge = json.enableUserBadge ?? true;
|
||||||
|
@ -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,40 +1,46 @@
|
||||||
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",
|
||||||
this._f = f;
|
"Calculates the distance between the feature and a specified point",
|
||||||
for (const key of keys) {
|
["longitude", "latitude"],
|
||||||
if (!key.startsWith('_')) {
|
(feature) => {
|
||||||
throw `Incorrect metakey ${key}: it should start with underscore (_)`
|
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 }[]) {
|
public static FullPatchFeature(feature) {
|
||||||
for (let i = 0; i < features.length; i++) {
|
for (const func of ExtraFunction.allFuncs) {
|
||||||
let feature = features[i];
|
func.PatchFeature(feature);
|
||||||
this._f(feature.feature, i, feature.freshness);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)>();
|
||||||
static GetCountryCodeFor(lon: number, lat: number, callback: (country: string) => void) {
|
for (const layer of layers) {
|
||||||
MetaTagging.coder.GetCountryCodeFor(lon, lat, callback)
|
layerFuncs.set(layer.id, this.createRetaggingFunc(layer));
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
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…
Reference in a new issue