Add switch to hide certain features, cleanup of code

This commit is contained in:
pietervdvn 2021-03-25 15:19:44 +01:00
parent aa0989b72a
commit 1b1ec9f15d
18 changed files with 230 additions and 173 deletions

View file

@ -24,6 +24,7 @@ export default class LayerConfig {
static WAYHANDLING_DEFAULT = 0;
static WAYHANDLING_CENTER_ONLY = 1;
static WAYHANDLING_CENTER_AND_WAY = 2;
id: string;
name: Translation
description: Translation;
@ -31,6 +32,7 @@ export default class LayerConfig {
calculatedTags: [string, string][]
doNotDownload: boolean;
passAllFeatures: boolean;
isShown: TagRenderingConfig;
minzoom: number;
maxzoom: number;
title?: TagRenderingConfig;
@ -205,6 +207,7 @@ export default class LayerConfig {
throw "Builtin SVG asset not found: " + iconPath
}
}
this.isShown = tr("isShown", "yes");
this.iconSize = tr("iconSize", "40,40,center");
this.color = tr("color", "#0000ff");
this.width = tr("width", "7");

View file

@ -1,5 +1,6 @@
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import {AndOrTagConfigJson} from "./TagConfigJson";
import TagRenderingConfig from "./TagRenderingConfig";
/**
* Configuration for a single layer
@ -51,7 +52,15 @@ export interface LayerConfigJson {
* Works well together with 'passAllFeatures', to add decoration
*/
doNotDownload?: boolean;
/**
* This tagrendering should either be 'yes' or 'no'. If 'no' is returned, then the feature will be hidden from view.
* This is useful to hide certain features from view
* The default value is 'yes'
*/
isShown?: TagRenderingConfigJson;
/**
* The zoomlevel at which point the data is shown and loaded.
* Default: 0

View file

@ -384,26 +384,7 @@ export class InitUiElements {
State.state.layerUpdater = updater;
const source = new FeaturePipeline(state.filteredLayers, updater, state.layoutToUse, state.changes, state.locationControl);
source.features.addCallbackAndRun((featuresFreshness: { feature: any, freshness: Date }[]) => {
if (featuresFreshness === undefined) {
return;
}
featuresFreshness.forEach(featureFresh => {
const feature = featureFresh.feature;
State.state.allElements.addOrGetElement(feature);
if (Hash.hash.data === feature.properties.id) {
State.state.selectedElement.setData(feature);
}
})
MetaTagging.addMetatags(featuresFreshness, state.layoutToUse.data.layers);
})
new ShowDataLayer(source.features, State.state.leafletMap,
State.state.layoutToUse);
new ShowDataLayer(source.features, State.state.leafletMap, State.state.layoutToUse);
new SelectedFeatureHandler(Hash.hash, State.state.selectedElement, source);

View file

@ -43,7 +43,7 @@ class TitleElement extends UIElement {
}
if (layer.source.osmTags.matchesProperties(properties)) {
const title = new TagRenderingAnswer(
this._allElementsStorage.getEventSourceFor(feature),
this._allElementsStorage.addOrGetElement(feature),
layer.title
)
return new Combine([defaultTitle, " | ", title]).Render();

View file

@ -15,30 +15,42 @@ export class ElementStorage {
this._elements[id] = eventSource;
}
addElement(element): UIEventSource<any> {
const eventSource = new UIEventSource<any>(element.properties, "tags of "+element.properties.id);
this._elements[element.properties.id] = eventSource;
return eventSource;
}
addOrGetElement(element: any) : UIEventSource<any>{
const elementId = element.properties.id;
/**
* Creates a UIEventSource for the tags of the given feature.
* If an UIEventsource has been created previously, the same UIEventSource will be returned
*
* Note: it will cleverly merge the tags, if needed
*/
addOrGetElement(feature: any): UIEventSource<any> {
const elementId = feature.properties.id;
if (elementId in this._elements) {
const es = this._elements[elementId];
if (es.data == feature.properties) {
// Reference comparison gives the same object! we can just return the event source
return es;
}
const keptKeys = es.data;
// The element already exists
// We add all the new keys to the old keys
for (const k in element.properties) {
const v = element.properties[k];
let somethingChanged = false;
for (const k in feature.properties) {
const v = feature.properties[k];
if (keptKeys[k] !== v) {
keptKeys[k] = v;
es.ping();
somethingChanged = true;
}
}
if (somethingChanged) {
es.ping();
}
return es;
}else{
return this.addElement(element);
} else {
const eventSource = new UIEventSource<any>(feature.properties, "tags of " + feature.properties.id);
this._elements[feature.properties.id] = eventSource;
return eventSource;
}
}
@ -48,8 +60,4 @@ export class ElementStorage {
}
console.error("Can not find eventsource with id ", elementId);
}
getEventSourceFor(feature): UIEventSource<any> {
return this.getEventSourceById(feature.properties.id);
}
}

81
Logic/ExtraFunction.ts Normal file
View file

@ -0,0 +1,81 @@
import {GeoOperations} from "./GeoOperations";
import {UIElement} from "../UI/UIElement";
import Combine from "../UI/Base/Combine";
export class ExtraFunction {
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;
}
public static FullPatchFeature(feature) {
for (const func of ExtraFunction.allFuncs) {
func.PatchFeature(feature);
}
}
public static HelpText(): UIElement {
return new Combine([
ExtraFunction.intro,
...ExtraFunction.allFuncs.map(func =>
new Combine([
"<h3>" + func._name + "</h3>",
func._doc,
"<ul>",
...func._args.map(arg => "<li>" + arg + "</li>"),
"</ul>"
])
)
]);
}
public PatchFeature(feature: any) {
feature[this._name] = this._f(feature);
}
static readonly intro = `<h2>Calculating tags with Javascript</h2>
<p>In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. <b>_lat</b>, <b>lon</b>, <b>_country</b>), as detailed above.</p>
<p>It is also possible to calculate your own tags - but this requires some javascript knowledge. </p>
Before proceeding, some warnings:
<ul>
<li> DO NOT DO THIS AS BEGINNER</li>
<li> <b>Only do this if all other techniques fail</b>. This should <i>not</i> be done to create a rendering effect, only to calculate a specific value</li>
<li> <b>THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES</b>. 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.</li>
</ul>
In the layer object, add a field <b>calculatedTags</b>, e.g.:
<div class="code">
"calculatedTags": {
"_someKey": "javascript-expression",
"name": "feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",
"_distanceCloserThen3Km": "feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'"
}
</div>
`
}

View file

@ -6,7 +6,7 @@ import LayerConfig from "../../Customizations/JSON/LayerConfig";
/**
* In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled)
* If this is the case, multiple objects with a different _matching_layer_id are generated.
* If not, the _feature_layter_id is added
* In any case, this featureSource marks the objects with _matching_layer_id
*/
export default class FeatureDuplicatorPerLayer implements FeatureSource {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;

View file

@ -12,6 +12,7 @@ import LocalStorageSource from "./LocalStorageSource";
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import Loc from "../../Models/Loc";
import GeoJsonSource from "./GeoJsonSource";
import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource";
export default class FeaturePipeline implements FeatureSource {
@ -25,42 +26,42 @@ export default class FeaturePipeline implements FeatureSource {
const amendedOverpassSource =
new RememberingSource(
new WayHandlingApplyingFeatureSource(flayers,
new NoOverlapSource(flayers, new FeatureDuplicatorPerLayer(flayers,
new LocalStorageSaver(updater, layout)))
)
new NoOverlapSource(flayers, new FeatureDuplicatorPerLayer(flayers,
new LocalStorageSaver(updater, layout)))
);
const geojsonSources: GeoJsonSource [] = []
for (const flayer of flayers.data) {
const sourceUrl = flayer.layerDef.source.geojsonSource
if (sourceUrl !== undefined) {
geojsonSources.push(new WayHandlingApplyingFeatureSource(flayers,
new GeoJsonSource(flayer.layerDef.id, sourceUrl)))
geojsonSources.push(
new GeoJsonSource(flayer.layerDef.id, sourceUrl))
}
}
const amendedLocalStorageSource =
new RememberingSource(
new WayHandlingApplyingFeatureSource(flayers,
new NoOverlapSource(flayers, new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout)))
));
new NoOverlapSource(flayers, new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout)))
);
newPoints = new FeatureDuplicatorPerLayer(flayers, newPoints);
const merged = new FeatureSourceMerger([
amendedOverpassSource,
amendedLocalStorageSource,
newPoints,
...geojsonSources
]);
const merged =
new MetaTaggingFeatureSource(
new FeatureSourceMerger([
amendedOverpassSource,
amendedLocalStorageSource,
newPoints,
...geojsonSources
]));
const source =
new FilteringFeatureSource(
flayers,
locationControl,
merged
);
new WayHandlingApplyingFeatureSource(flayers,
new FilteringFeatureSource(
flayers,
locationControl,
merged
));
this.features = source.features;
}

View file

@ -29,6 +29,7 @@ export default class FilteringFeatureSource implements FeatureSource {
const newFeatures = features.filter(f => {
const layerId = f.feature._matching_layer_id;
if (layerId !== undefined) {
const layer: {
isDisplayed: UIEventSource<boolean>,
@ -38,6 +39,17 @@ export default class FilteringFeatureSource implements FeatureSource {
console.error("No layer found with id ", layerId);
return true;
}
const isShown = layer.layerDef.isShown
const tags = f.feature.properties;
console.log("Is shown: ", isShown," known? ", isShown.IsKnown(tags), " result: ", isShown.GetRenderValue(tags).txt)
if(isShown.IsKnown(tags)){
const result = layer.layerDef.isShown.GetRenderValue(f.feature.properties).txt;
if(result !== "yes"){
return false;
}
}
if (FilteringFeatureSource.showLayer(layer, location)) {
return true;
}

View file

@ -0,0 +1,30 @@
import FeatureSource from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import State from "../../State";
import Hash from "../Web/Hash";
import MetaTagging from "../MetaTagging";
export default class MetaTaggingFeatureSource implements FeatureSource {
features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{feature: any; freshness: Date}[]>(undefined);
constructor(source: FeatureSource) {
const self = this;
source.features.addCallbackAndRun((featuresFreshness: { feature: any, freshness: Date }[]) => {
if (featuresFreshness === undefined) {
return;
}
featuresFreshness.forEach(featureFresh => {
const feature = featureFresh.feature;
State.state.allElements.addOrGetElement(feature);
if (Hash.hash.data === feature.properties.id) {
State.state.selectedElement.setData(feature);
}
})
MetaTagging.addMetatags(featuresFreshness, State.state.layoutToUse.data.layers);
self.features.setData(featuresFreshness);
});
}
}

View file

@ -3,6 +3,9 @@ import {UIEventSource} from "../UIEventSource";
import LayerConfig from "../../Customizations/JSON/LayerConfig";
import {GeoOperations} from "../GeoOperations";
/**
* This is the part of the pipeline which introduces extra points at the center of an area (but only if this is demanded by the wayhandling)
*/
export default class WayHandlingApplyingFeatureSource implements FeatureSource {
features: UIEventSource<{ feature: any; freshness: Date }[]>;
@ -46,6 +49,7 @@ export default class WayHandlingApplyingFeatureSource implements FeatureSource {
continue;
}
// Create the copy
const centerPoint = GeoOperations.centerpoint(feat);
centerPoint._matching_layer_id = feat._matching_layer_id;
newFeatures.push({feature: centerPoint, freshness: f.freshness});

View file

@ -1,85 +1,6 @@
import {GeoOperations} from "./GeoOperations";
import LayerConfig from "../Customizations/JSON/LayerConfig";
import SimpleMetaTagger from "./SimpleMetaTagger";
import {UIElement} from "../UI/UIElement";
import Combine from "../UI/Base/Combine";
export class ExtraFunction {
static readonly intro = `<h2>Calculating tags with Javascript</h2>
<p>In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. <b>_lat</b>, <b>lon</b>, <b>_country</b>), as detailed above.</p>
<p>It is also possible to calculate your own tags - but this requires some javascript knowledge. </p>
Before proceeding, some warnings:
<ul>
<li> DO NOT DO THIS AS BEGINNER</li>
<li> <b>Only do this if all other techniques fail</b>. This should <i>not</i> be done to create a rendering effect, only to calculate a specific value</li>
<li> <b>THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES</b>. 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.</li>
</ul>
In the layer object, add a field <b>calculatedTags</b>, e.g.:
<div class="code">
"calculatedTags": {
"_someKey": "javascript-expression",
"name": "feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",
"_distanceCloserThen3Km": "feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'"
}
</div>
`
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;
}
public static FullPatchFeature(feature) {
for (const func of ExtraFunction.allFuncs) {
func.PatchFeature(feature);
}
}
public static HelpText(): UIElement {
return new Combine([
ExtraFunction.intro,
...ExtraFunction.allFuncs.map(func =>
new Combine([
"<h3>" + func._name + "</h3>",
func._doc,
"<ul>",
...func._args.map(arg => "<li>" + arg + "</li>"),
"</ul>"
])
)
]);
}
public PatchFeature(feature: any) {
feature[this._name] = this._f(feature);
}
}
import {ExtraFunction} from "./ExtraFunction";
/**
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
@ -163,4 +84,6 @@ export default class MetaTagging {
}
}
}
}

View file

@ -7,30 +7,15 @@ import {TagsFilter} from "../TagsFilter";
* Interfaces overpass to get all the latest data
*/
export class Overpass {
private _filter: TagsFilter
public static testUrl: string = null
private readonly _extraScripts: string[];
private _filter: TagsFilter
private readonly _extraScripts: string[];
constructor(filter: TagsFilter, extraScripts: string[]) {
this._filter = filter
this._extraScripts = extraScripts;
}
private buildQuery(bbox: string): string {
const filters = this._filter.asOverpass()
let filter = ""
for (const filterOr of filters) {
filter += 'nwr' + filterOr + ';'
}
for (const extraScript of this._extraScripts){
filter += '('+extraScript+');';
}
const query =
'[out:json][timeout:25]' + bbox + ';(' + filter + ');out body;>;out skel qt;'
return "https://overpass-api.de/api/interpreter?data=" + encodeURIComponent(query)
}
queryGeoJson(bounds: Bounds, continuation: ((any, date: Date) => void), onFail: ((reason) => void)): void {
let query = this.buildQuery("[bbox:" + bounds.south + "," + bounds.west + "," + bounds.north + "," + bounds.east + "]")
@ -52,11 +37,27 @@ export class Overpass {
onFail("Runtime error (timeout)")
return;
}
// @ts-ignore
const geojson = OsmToGeoJson.default(json);
console.log("Received geojson", geojson)
const osmTime = new Date(json.osm3s.timestamp_osm_base);
continuation(geojson, osmTime);
}).fail(onFail)
}
private buildQuery(bbox: string): string {
const filters = this._filter.asOverpass()
let filter = ""
for (const filterOr of filters) {
filter += 'nwr' + filterOr + ';'
}
for (const extraScript of this._extraScripts) {
filter += '(' + extraScript + ');';
}
const query =
'[out:json][timeout:25]' + bbox + ';(' + filter + ');out body;>;out skel qt;'
return "https://overpass-api.de/api/interpreter?data=" + encodeURIComponent(query)
}
}

View file

@ -64,7 +64,7 @@ export default class SimpleMetaTagger {
SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => {
try {
feature.properties["_country"] = countries[0].trim().toLowerCase();
const tagsSource = State.state.allElements.getEventSourceFor(feature);
const tagsSource = State.state.allElements.addOrGetElement(feature);
tagsSource.ping();
} catch (e) {
console.warn(e)
@ -77,7 +77,7 @@ export default class SimpleMetaTagger {
"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);
const tagsSource = State.state.allElements.addOrGetElement(feature);
tagsSource.addCallbackAndRun(tags => {
if (tags.opening_hours === undefined || tags._country === undefined) {
return;

View file

@ -86,7 +86,7 @@ export default class ShowDataLayer {
marker.openPopup();
const popup = marker.getPopup();
const tags = State.state.allElements.getEventSourceFor(selected);
const tags = State.state.allElements.addOrGetElement(selected);
const layer: LayerConfig = this._layerDict[selected._matching_layer_id];
const infoBox = FeatureInfoBox.construct(tags, layer);
@ -105,7 +105,7 @@ export default class ShowDataLayer {
private createStyleFor(feature) {
const tagsSource = State.state.allElements.getEventSourceFor(feature);
const tagsSource = State.state.allElements.addOrGetElement(feature);
// Every object is tied to exactly one layer
const layer = this._layerDict[feature._matching_layer_id];
return layer?.GenerateLeafletStyle(tagsSource, layer._showOnPopup !== undefined);

View file

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

View file

@ -6,13 +6,19 @@
"minzoom": 16,
"source": {
"osmTags": {
"or": [
"highway=pedestrian",
"highway=footway",
"highway=path",
"highway=bridleway",
"highway=living_street",
"highway=track"
"and": [
{
"or": [
"highway=pedestrian",
"highway=footway",
"highway=path",
"highway=bridleway",
"highway=living_street",
"highway=track"
]
},
"access!=no",
"access!=private"
]
}
},
@ -51,7 +57,8 @@
"nl": "Woonerf"
}
},
{"if": "highway=path",
{
"if": "highway=path",
"then": "Klein pad"
}
]

View file

@ -4,8 +4,8 @@ import SpecialVisualizations from "../UI/SpecialVisualizations";
import {writeFileSync} from "fs";
import {UIElement} from "../UI/UIElement";
import SimpleMetaTagger from "../Logic/SimpleMetaTagger";
import {ExtraFunction} from "../Logic/MetaTagging";
import Combine from "../UI/Base/Combine";
import {ExtraFunction} from "../Logic/ExtraFunction";