Add custom javascript snippets to calculate tags

This commit is contained in:
pietervdvn 2021-03-24 01:25:57 +01:00
parent 3e130ebe80
commit f124d9ded7
17 changed files with 799 additions and 14649 deletions

View file

@ -28,6 +28,7 @@ export default class LayerConfig {
name: Translation
description: Translation;
source: SourceConfig;
calculatedTags: [string, string][]
doNotDownload: boolean;
passAllFeatures: boolean;
minzoom: number;
@ -51,8 +52,9 @@ export default class LayerConfig {
}[];
tagRenderings: TagRenderingConfig [];
constructor(json: LayerConfigJson,
official: boolean= true,
context?: string) {
context = context + "." + json.id;
const self = this;
@ -65,9 +67,9 @@ export default class LayerConfig {
// @ts-ignore
legacy = FromJSON.Tag(json["overpassTags"], context + ".overpasstags");
}
if(json.source !== undefined){
if (legacy !== undefined ) {
throw context+"Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined"
if (json.source !== undefined) {
if (legacy !== undefined) {
throw context + "Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined"
}
let osmTags: TagsFilter = legacy;
@ -81,15 +83,22 @@ export default class LayerConfig {
geojsonSource: json.source["geoJsonSource"],
overpassScript: json.source["overpassScript"],
});
}else{
} else {
this.source = new SourceConfig({
osmTags : legacy
osmTags: legacy
})
}
this.calculatedTags = undefined;
if (json.calculatedTags !== undefined) {
console.warn("Unofficial theme with custom javascript! This is a security risk")
this.calculatedTags = [];
for (const key in json.calculatedTags) {
this.calculatedTags.push([key, json.calculatedTags[key]])
}
}
this.doNotDownload = json.doNotDownload ?? false;
this.passAllFeatures = json.passAllFeatures ?? false;
this.minzoom = json.minzoom ?? 0;
@ -139,10 +148,10 @@ export default class LayerConfig {
if (typeof renderingJson === "string") {
if (renderingJson === "questions") {
if(readOnly){
if (readOnly) {
throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}`
}
return new TagRenderingConfig("questions", undefined)
}
@ -203,6 +212,13 @@ export default class LayerConfig {
}
public CustomCodeSnippets(): string[]{
if(this.calculatedTags === undefined){
return []
}
return this.calculatedTags.map(code => code[1]);
}
public AddRoamingRenderings(addAll: {
tagRenderings: TagRenderingConfig[],

View file

@ -41,6 +41,11 @@ export interface LayerConfigJson {
*/
source: {osmTags: AndOrTagConfigJson | string} | {geoJsonSource: string} | {overpassScript: string}
/**
* A dictionary of 'key': 'js-expression'. These js-expressions will be calculated for every feature, giving extra tags to work with in the rest of the pipieline
*/
calculatedTags? : any;
/**
* If set, this layer will not query overpass; but it'll still match the tags above which are by chance returned by other layers.
* Works well together with 'passAllFeatures', to add decoration

View file

@ -31,7 +31,7 @@ export default class LayoutConfig {
};
public readonly hideFromOverview: boolean;
public readonly lockLocation: boolean | [[number,number],[number, number]];
public readonly lockLocation: boolean | [[number, number], [number, number]];
public readonly enableUserBadge: boolean;
public readonly enableShareScreen: boolean;
public readonly enableMoreQuests: boolean;
@ -39,10 +39,12 @@ export default class LayoutConfig {
public readonly enableLayers: boolean;
public readonly enableSearch: boolean;
public readonly enableGeolocation: boolean;
private readonly _official : boolean;
public readonly enableBackgroundLayerSelection: boolean;
public readonly customCss?: string;
constructor(json: LayoutConfigJson, context?: string) {
constructor(json: LayoutConfigJson, official=true, context?: string) {
this._official = official;
this.id = json.id;
context = (context ?? "") + "." + this.id;
this.maintainer = json.maintainer;
@ -54,7 +56,7 @@ export default class LayoutConfig {
} else {
this.language = json.language;
}
if(this.language.length == 0){
if (this.language.length == 0) {
throw "No languages defined. Define at least one language"
}
if (json.title === undefined) {
@ -66,7 +68,7 @@ export default class LayoutConfig {
this.title = new Translation(json.title, context + ".title");
this.description = new Translation(json.description, context + ".description");
this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, context + ".shortdescription");
this.descriptionTail = json.descriptionTail === undefined ? new Translation({"*": ""}, context+".descriptionTail") : new Translation(json.descriptionTail, context + ".descriptionTail");
this.descriptionTail = json.descriptionTail === undefined ? new Translation({"*": ""}, context + ".descriptionTail") : new Translation(json.descriptionTail, context + ".descriptionTail");
this.icon = json.icon;
this.socialImage = json.socialImage;
this.startZoom = json.startZoom;
@ -79,7 +81,7 @@ export default class LayoutConfig {
return SharedTagRenderings.SharedTagRendering[tr];
}
}
return new TagRenderingConfig(tr, undefined,`${this.id}.roaming_renderings[${i}]`);
return new TagRenderingConfig(tr, undefined, `${this.id}.roaming_renderings[${i}]`);
}
);
this.defaultBackgroundId = json.defaultBackgroundId;
@ -104,32 +106,31 @@ export default class LayoutConfig {
}
// @ts-ignore
return new LayerConfig(layer, `${this.id}.layers[${i}]`)
return new LayerConfig(layer, official,`${this.id}.layers[${i}]`)
});
// ALl the layers are constructed, let them share tags in now!
const roaming : {r, source: LayerConfig}[] = []
const roaming: { r, source: LayerConfig }[] = []
for (const layer of this.layers) {
roaming.push({r: layer.GetRoamingRenderings(), source:layer});
roaming.push({r: layer.GetRoamingRenderings(), source: layer});
}
for (const layer of this.layers) {
for (const r of roaming) {
if(r.source == layer){
if (r.source == layer) {
continue;
}
layer.AddRoamingRenderings(r.r);
}
}
for(const layer of this.layers) {
for (const layer of this.layers) {
layer.AddRoamingRenderings(
{
titleIcons:[],
titleIcons: [],
iconOverlays: [],
tagRenderings: this.roamingRenderings
}
}
);
}
@ -151,8 +152,8 @@ export default class LayoutConfig {
this.hideFromOverview = json.hideFromOverview ?? false;
// @ts-ignore
if(json.hideInOverview){
throw "The json for "+this.id+" contains a 'hideInOverview'. Did you mean hideFromOverview instead?"
if (json.hideInOverview) {
throw "The json for " + this.id + " contains a 'hideInOverview'. Did you mean hideFromOverview instead?"
}
this.lockLocation = json.lockLocation ?? false;
this.enableUserBadge = json.enableUserBadge ?? true;
@ -166,4 +167,19 @@ export default class LayoutConfig {
this.customCss = json.customCss;
}
public CustomCodeSnippets(): string[] {
if(this._official){
return [];
}
const msg = "<br/><b>This layout uses <span class='alert'>custom javascript</span>, loaded for the wide internet. The code is printed below, please report suspicious code on the issue tracker of MapComplete:</b><br/>"
const custom = [];
for (const layer of this.layers) {
custom.push(...layer.CustomCodeSnippets().map(code => code+"<br />"))
}
if (custom.length === 0) {
return custom;
}
custom.splice(0, 0, msg);
return custom;
}
}

View 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"
}
```

View file

@ -209,7 +209,7 @@ export class InitUiElements {
hashFromLocalStorage.setData(hash);
dedicatedHashFromLocalStorage.setData(hash);
}
const layoutToUse = new LayoutConfig(JSON.parse(atob(hash)));
const layoutToUse = new LayoutConfig(JSON.parse(atob(hash)), false);
userLayoutParam.setData(layoutToUse.id);
return layoutToUse;
} catch (e) {
@ -380,7 +380,6 @@ export class InitUiElements {
return flayers;
});
const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap);
State.state.layerUpdater = updater;
const source = new FeaturePipeline(state.filteredLayers, updater, state.layoutToUse, state.changes, state.locationControl);
@ -399,7 +398,8 @@ export class InitUiElements {
}
})
MetaTagging.addMetatags(featuresFreshness);
MetaTagging.addMetatags(featuresFreshness, state.layoutToUse.data.layers);
})
new ShowDataLayer(source.features, State.state.leafletMap,

View file

@ -24,7 +24,7 @@ export default class InstalledThemes {
try {
const json = atob(customLayout.data);
const layout = new LayoutConfig(
JSON.parse(json));
JSON.parse(json), false);
installedThemes.push({
layout: layout,
definition: customLayout.data

View file

@ -21,6 +21,15 @@ export class GeoOperations {
return coordinates;
}
/**
* Returns the distance between the two points in kilometers
* @param lonlat0
* @param lonlat1
*/
static distanceBetween(lonlat0: [number,number], lonlat1:[number, number]){
return turf.distance(lonlat0, lonlat1)
}
static featureIsContainedInAny(feature: any,
shouldNotContain: any[],
maxOverlapPercentage: number): boolean {

View file

@ -1,40 +1,46 @@
import {GeoOperations} from "./GeoOperations";
import State from "../State";
import opening_hours from "opening_hours";
import {Or} from "./Or";
import {Utils} from "../Utils";
import {UIElement} from "../UI/UIElement";
import Combine from "../UI/Base/Combine";
import {Tag} from "./Tag";
import {And} from "./And";
import LayerConfig from "../Customizations/JSON/LayerConfig";
import SimpleMetaTagger from "./SimpleMetaTagger";
class SimpleMetaTagger {
public readonly keys: string[];
public readonly doc: string;
private readonly _f: (feature: any, index: number, freshness: Date) => void;
export class ExtraFunction {
constructor(keys: string[], doc: string, f: ((feature: any, index: number, freshness: Date) => void)) {
this.keys = keys;
this.doc = doc;
this._f = f;
for (const key of keys) {
if (!key.startsWith('_')) {
throw `Incorrect metakey ${key}: it should start with underscore (_)`
static readonly doc: string = "When the feature is downloaded, some extra tags can be calculated by a javascript snippet. The feature is passed as 'feat'; there are a few functions available on it to handle it - apart from 'feat.tags' which is a classic object containing all the tags."
private static DistanceToFunc = new ExtraFunction(
"distanceTo",
"Calculates the distance between the feature and a specified point",
["longitude", "latitude"],
(feature) => {
return (lon, lat) => {
// Feature._lon and ._lat is conveniently place by one of the other metatags
return GeoOperations.distanceBetween([lon, lat], [feature._lon, feature._lat]);
}
}
)
private static readonly allFuncs : ExtraFunction[] = [ExtraFunction.DistanceToFunc];
private readonly _name: string;
private readonly _args: string[];
private readonly _doc: string;
private readonly _f: (feat: any) => any;
constructor(name: string, doc: string, args: string[], f: ((feat: any) => any)) {
this._name = name;
this._doc = doc;
this._args = args;
this._f = f;
}
addMetaTags(features: { feature: any, freshness: Date }[]) {
for (let i = 0; i < features.length; i++) {
let feature = features[i];
this._f(feature.feature, i, feature.freshness);
public static FullPatchFeature(feature) {
for (const func of ExtraFunction.allFuncs) {
func.PatchFeature(feature);
}
}
public PatchFeature(feature: any) {
feature[this._name] = this._f(feature);
}
}
/**
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
*
@ -43,282 +49,13 @@ class SimpleMetaTagger {
export default class MetaTagging {
static coder: any;
private static latlon = new SimpleMetaTagger(["_lat", "_lon"], "The latitude and longitude of the point (or centerpoint in the case of a way/area)",
(feature => {
const centerPoint = GeoOperations.centerpoint(feature);
const lat = centerPoint.geometry.coordinates[1];
const lon = centerPoint.geometry.coordinates[0];
feature.properties["_lat"] = "" + lat;
feature.properties["_lon"] = "" + lon;
})
);
private static surfaceArea = new SimpleMetaTagger(
["_surface", "_surface:ha"], "The surface area of the feature, in square meters and in hectare. Not set on points and ways",
(feature => {
const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature);
feature.properties["_surface"] = "" + sqMeters;
feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10;
})
);
private static country = new SimpleMetaTagger(
["_country"], "The country code of the property (with latlon2country)",
feature => {
let centerPoint: any = GeoOperations.centerpoint(feature);
const lat = centerPoint.geometry.coordinates[1];
const lon = centerPoint.geometry.coordinates[0];
MetaTagging.GetCountryCodeFor(lon, lat, (countries) => {
try {
feature.properties["_country"] = countries[0].trim().toLowerCase();
const tagsSource = State.state.allElements.getEventSourceFor(feature);
tagsSource.ping();
} catch (e) {
console.warn(e)
}
});
}
)
private static isOpen = new SimpleMetaTagger(
["_isOpen", "_isOpen:description"],
"If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')",
(feature => {
const tagsSource = State.state.allElements.getEventSourceFor(feature);
tagsSource.addCallbackAndRun(tags => {
if (tags.opening_hours === undefined || tags._country === undefined) {
return;
}
try {
const oh = new opening_hours(tags["opening_hours"], {
lat: tags._lat,
lon: tags._lon,
address: {
country_code: tags._country.toLowerCase()
}
}, {tag_key: "opening_hours"});
// AUtomatically triggered on the next change
const updateTags = () => {
const oldValueIsOpen = tags["_isOpen"];
const oldNextChange = tags["_isOpen:nextTrigger"] ?? 0;
if (oldNextChange > (new Date()).getTime() &&
tags["_isOpen:oldvalue"] === tags["opening_hours"]) {
// Already calculated and should not yet be triggered
return;
}
tags["_isOpen"] = oh.getState() ? "yes" : "no";
const comment = oh.getComment();
if (comment) {
tags["_isOpen:description"] = comment;
}
if (oldValueIsOpen !== tags._isOpen) {
tagsSource.ping();
}
const nextChange = oh.getNextChange();
if (nextChange !== undefined) {
const timeout = nextChange.getTime() - (new Date()).getTime();
tags["_isOpen:nextTrigger"] = nextChange.getTime();
tags["_isOpen:oldvalue"] = tags.opening_hours
window.setTimeout(
() => {
console.log("Updating the _isOpen tag for ", tags.id, ", it's timer expired after", timeout);
updateTags();
},
timeout
)
}
}
updateTags();
} catch (e) {
console.warn("Error while parsing opening hours of ", tags.id, e);
tags["_isOpen"] = "parse_error";
}
})
})
)
private static directionSimplified = new SimpleMetaTagger(
["_direction:simplified", "_direction:leftright"], "_direction:simplified turns 'camera:direction' and 'direction' into either 0, 45, 90, 135, 180, 225, 270 or 315, whichever is closest. _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map",
(feature => {
const tags = feature.properties;
const direction = tags["camera:direction"] ?? tags["direction"];
if (direction === undefined) {
return;
}
let n = Number(direction);
if (isNaN(n)) {
return;
}
// [22.5 -> 67.5] is sector 1
// [67.5 -> ] is sector 1
n = (n + 22.5) % 360;
n = Math.floor(n / 45);
tags["_direction:simplified"] = n;
tags["_direction:leftright"] = n <= 3 ? "right" : "left";
})
)
private static carriageWayWidth = new SimpleMetaTagger(
["_width:needed", "_width:needed:no_pedestrians", "_width:difference"],
"Legacy for a specific project calculating the needed width for safe traffic on a road. Only activated if 'width:carriageway' is present",
(feature: any, index: number) => {
const properties = feature.properties;
if (properties["width:carriageway"] === undefined) {
return;
}
const carWidth = 2;
const cyclistWidth = 1.5;
const pedestrianWidth = 0.75;
const _leftSideParking =
new And([new Tag("parking:lane:left", "parallel"), new Tag("parking:lane:right", "no_parking")]);
const _rightSideParking =
new And([new Tag("parking:lane:right", "parallel"), new Tag("parking:lane:left", "no_parking")]);
const _bothSideParking = new Tag("parking:lane:both", "parallel");
const _noSideParking = new Tag("parking:lane:both", "no_parking");
const _otherParkingMode =
new Or([
new Tag("parking:lane:both", "perpendicular"),
new Tag("parking:lane:left", "perpendicular"),
new Tag("parking:lane:right", "perpendicular"),
new Tag("parking:lane:both", "diagonal"),
new Tag("parking:lane:left", "diagonal"),
new Tag("parking:lane:right", "diagonal"),
])
const _sidewalkBoth = new Tag("sidewalk", "both");
const _sidewalkLeft = new Tag("sidewalk", "left");
const _sidewalkRight = new Tag("sidewalk", "right");
const _sidewalkNone = new Tag("sidewalk", "none");
let parallelParkingCount = 0;
const _oneSideParking = new Or([_leftSideParking, _rightSideParking]);
if (_oneSideParking.matchesProperties(properties)) {
parallelParkingCount = 1;
} else if (_bothSideParking.matchesProperties(properties)) {
parallelParkingCount = 2;
} else if (_noSideParking.matchesProperties(properties)) {
parallelParkingCount = 0;
} else if (_otherParkingMode.matchesProperties(properties)) {
parallelParkingCount = 0;
} else {
console.log("No parking data for ", properties.name, properties.id, properties)
}
let pedestrianFlowNeeded;
if (_sidewalkBoth.matchesProperties(properties)) {
pedestrianFlowNeeded = 0;
} else if (_sidewalkNone.matchesProperties(properties)) {
pedestrianFlowNeeded = 2;
} else if (_sidewalkLeft.matchesProperties(properties) || _sidewalkRight.matchesProperties(properties)) {
pedestrianFlowNeeded = 1;
} else {
pedestrianFlowNeeded = -1;
}
let onewayCar = properties.oneway === "yes";
let onewayBike = properties["oneway:bicycle"] === "yes" ||
(onewayCar && properties["oneway:bicycle"] === undefined)
let cyclingAllowed =
!(properties.bicycle === "use_sidepath"
|| properties.bicycle === "no");
let carWidthUsed = (onewayCar ? 1 : 2) * carWidth;
properties["_width:needed:cars"] = Utils.Round(carWidthUsed);
properties["_width:needed:parking"] = Utils.Round(parallelParkingCount * carWidth)
let cyclistWidthUsed = 0;
if (cyclingAllowed) {
cyclistWidthUsed = (onewayBike ? 1 : 2) * cyclistWidth;
}
properties["_width:needed:cyclists"] = Utils.Round(cyclistWidthUsed)
const width = parseFloat(properties["width:carriageway"]);
const targetWidthIgnoringPedestrians =
carWidthUsed +
cyclistWidthUsed +
parallelParkingCount * carWidthUsed;
properties["_width:needed:no_pedestrians"] = Utils.Round(targetWidthIgnoringPedestrians);
const pedestriansNeed = Math.max(0, pedestrianFlowNeeded) * pedestrianWidth;
const targetWidth = targetWidthIgnoringPedestrians + pedestriansNeed;
properties["_width:needed"] = Utils.Round(targetWidth);
properties["_width:needed:pedestrians"] = Utils.Round(pedestriansNeed)
properties["_width:difference"] = Utils.Round(targetWidth - width);
properties["_width:difference:no_pedestrians"] = Utils.Round(targetWidthIgnoringPedestrians - width);
}
);
private static currentTime = new SimpleMetaTagger(
["_now:date", "_now:datetime", "_loaded:date", "_loaded:_datetime"],
"Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely",
(feature, _, freshness) => {
const now = new Date();
if (typeof freshness === "string") {
freshness = new Date(freshness)
}
function date(d: Date) {
return d.toISOString().slice(0, 10);
}
function datetime(d: Date) {
return d.toISOString().slice(0, -5).replace("T", " ");
}
feature.properties["_now:date"] = date(now);
feature.properties["_now:datetime"] = datetime(now);
feature.properties["_loaded:date"] = date(freshness);
feature.properties["_loaded:datetime"] = datetime(freshness);
}
)
private static metatags = [
MetaTagging.latlon,
MetaTagging.surfaceArea,
MetaTagging.country,
MetaTagging.isOpen,
MetaTagging.carriageWayWidth,
MetaTagging.directionSimplified,
MetaTagging.currentTime
];
/**
* An actor which adds metatags on every feature in the given object
* The features are a list of geojson-features, with a "properties"-field and geometry
*/
static addMetatags(features: { feature: any, freshness: Date }[]) {
static addMetatags(features: { feature: any; freshness: Date }[], layers: LayerConfig[]) {
for (const metatag of MetaTagging.metatags) {
for (const metatag of SimpleMetaTagger.metatags) {
try {
metatag.addMetaTags(features);
} catch (e) {
@ -327,32 +64,63 @@ export default class MetaTagging {
}
}
}
static GetCountryCodeFor(lon: number, lat: number, callback: (country: string) => void) {
MetaTagging.coder.GetCountryCodeFor(lon, lat, callback)
}
static HelpText(): UIElement {
const subElements: UIElement[] = [
new Combine([
"<h1>Metatags</h1>",
"Metatags are extra tags available, in order to display more data or to give better questions.",
"The are calculated when the data arrives in the webbrowser. This document gives an overview of the available metatags"
])
];
for (const metatag of MetaTagging.metatags) {
subElements.push(
new Combine([
"<h3>", metatag.keys.join(", "), "</h3>",
metatag.doc]
)
)
// The functions - per layer - which add the new keys
const layerFuncs = new Map<string, ((feature: any) => void)>();
for (const layer of layers) {
layerFuncs.set(layer.id, this.createRetaggingFunc(layer));
}
return new Combine(subElements)
for (const feature of features) {
// @ts-ignore
const key = feature.feature._matching_layer_id;
const f = layerFuncs.get(key);
if (f === undefined) {
continue;
}
f(feature.feature)
}
}
private static createRetaggingFunc(layer: LayerConfig): ((feature: any) => void) {
const calculatedTags: [string, string][] = layer.calculatedTags;
if (calculatedTags === undefined) {
return undefined;
}
const functions: ((feature: any) => void)[] = [];
for (const entry of calculatedTags) {
const key = entry[0]
const code = entry[1];
if (code === undefined) {
continue;
}
const func = new Function("feat", "return " + code + ";");
const f = (feature: any) => {
feature.properties[key] = func(feature);
}
functions.push(f)
}
return (feature) => {
const tags = feature.properties
if (tags === undefined) {
return;
}
ExtraFunction.FullPatchFeature(feature);
for (const f of functions) {
try {
f(feature);
} catch (e) {
console.error("While calculating a tag value: ", e)
}
}
}
}
}

333
Logic/SimpleMetaTagger.ts Normal file
View 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)
}
}

View file

@ -2,7 +2,7 @@ import { Utils } from "../Utils";
export default class Constants {
public static vNumber = "0.6.0a";
public static vNumber = "0.6.1";
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {
@ -12,7 +12,7 @@ export default class Constants {
tagsVisibleAt: 25,
mapCompleteHelpUnlock: 50,
tagsVisibleAndWikiLinked: 30,
themeGeneratorReadOnlyUnlock: 100,
themeGeneratorReadOnlyUnlock: 50,
themeGeneratorFullUnlock: 500,
addNewPointWithUnreadMessagesUnlock: 500,
minZoomLevelToAddNewPoints: (Constants.isRetina() ? 18 : 19)

View file

@ -5,52 +5,54 @@ import Combine from "../Base/Combine";
import LanguagePicker from "../LanguagePicker";
import Translations from "../i18n/Translations";
import {VariableUiElement} from "../Base/VariableUIElement";
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import {UIEventSource} from "../../Logic/UIEventSource";
export default class ThemeIntroductionPanel extends UIElement {
private languagePicker: UIElement;
private readonly description: UIElement;
private readonly plzLogIn: UIElement;
private readonly welcomeBack: UIElement;
private readonly tail: UIElement;
private readonly loginStatus: UIElement;
private _layout: UIEventSource<LayoutConfig>;
constructor() {
super(State.state.osmConnection.userDetails);
this.ListenTo(Locale.language);
this.languagePicker = LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language, Translations.t.general.pickLanguage);
const layout = State.state.layoutToUse.data;
this._layout = State.state.layoutToUse;
this.ListenTo(State.state.layoutToUse);
this.description = layout.description
this.plzLogIn =
const plzLogIn =
Translations.t.general.loginWithOpenStreetMap
.onClick(() => {
State.state.osmConnection.AttemptLogin()
});
this.welcomeBack = Translations.t.general.welcomeBack;
this.tail = layout.descriptionTail;
const welcomeBack = Translations.t.general.welcomeBack;
this.loginStatus = new VariableUiElement(
State.state.osmConnection.userDetails.map(
userdetails => {
if (State.state.featureSwitchUserbadge.data) {
return "";
}
return (userdetails.loggedIn ? this.welcomeBack : this.plzLogIn).Render();
return (userdetails.loggedIn ? welcomeBack : plzLogIn).Render();
}
)
)
}
InnerRender(): string {
const layout : LayoutConfig = this._layout.data;
return new Combine([
this.description,
layout.description,
"<br/><br/>",
this.loginStatus,
this.tail,
layout.descriptionTail,
"<br/>",
this.languagePicker
this.languagePicker,
...layout.CustomCodeSnippets()
]).Render()
}

View file

@ -15,6 +15,9 @@
"source": {
"osmTags": "amenity=public_bookcase"
},
"calculatedTags": {
"_distanceToPietervdn": "feat.distanceTo(3.704388, 51.05281) < 1 ? 'closeby' : 'faraway'"
},
"minzoom": 12,
"wayHandling": 2,
"title": {

View file

@ -50,7 +50,7 @@
{
"if": {
"and": [
"name:nl~"
"name:nl~*"
]
},
"then": {

View file

@ -14,10 +14,10 @@ import Translations from "./UI/i18n/Translations";
import CountryCoder from "latlon2country"
import MetaTagging from "./Logic/MetaTagging";
import SimpleMetaTagger from "./Logic/SimpleMetaTagger";
// Workaround for a stupid crash: inject the function
MetaTagging.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
let defaultLayout = ""
@ -87,7 +87,7 @@ if (layoutFromBase64.startsWith("http")) {
const parsed = JSON.parse(data);
// Overwrite the id to the wiki:value
parsed.id = link;
const layout = new LayoutConfig(parsed);
const layout = new LayoutConfig(parsed, false);
InitUiElements.InitAll(layout, layoutFromBase64, testing, layoutFromBase64, btoa(data));
} catch (e) {
new FixedUiElement(`<a href="${link}">${link}</a> is invalid:<br/>${e}<br/> <a href='https://${window.location.host}/'>Go back</a>")`)

14520
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -32,6 +32,7 @@
"dependencies": {
"@babel/preset-env": "7.13.8",
"@tailwindcss/postcss7-compat": "^2.0.2",
"@turf/distance": "^6.3.0",
"@types/jquery": "^3.5.5",
"@types/leaflet-markercluster": "^1.0.3",
"@types/leaflet-providers": "^1.2.0",

View file

@ -1,9 +1,10 @@
import {Utils} from "../Utils";
Utils.runningFromConsole = true;
import SpecialVisualizations from "../UI/SpecialVisualizations";
import {existsSync, mkdirSync, readFileSync, writeFile, writeFileSync} from "fs";
import {writeFileSync} from "fs";
import {UIElement} from "../UI/UIElement";
import MetaTagging from "../Logic/MetaTagging";
import SimpleMetaTagger from "../Logic/SimpleMetaTagger";
Utils.runningFromConsole = true;
const TurndownService = require('turndown')
@ -14,7 +15,7 @@ function WriteFile(filename, html: UIElement) : void {
}
WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage)
WriteFile("./Docs/CalculatedTags.md", MetaTagging.HelpText())
WriteFile("./Docs/CalculatedTags.md", SimpleMetaTagger.HelpText())
console.log("Generated docs")