From e42a668c4a76fb37a887f02d719d66efe13a1e69 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 3 Jan 2021 03:09:52 +0100 Subject: [PATCH] More refactoring: using a decent, configurable datapipeline now --- Customizations/JSON/LayerConfig.ts | 2 +- Customizations/JSON/LayerConfigJson.ts | 3 +- InitUiElements.ts | 59 ++++++-- Logic/Actors/StrayClickHandler.ts | 3 +- Logic/{ => Actors}/UpdateFromOverpass.ts | 24 ++-- .../FeatureSource.ts | 5 +- Logic/FeatureSource/FeatureSourceMerger.ts | 40 ++++++ Logic/FeatureSource/FilteringFeatureSource.ts | 52 +++++++ Logic/FeatureSource/NoOverlapSource.ts | 86 ++++++++++++ Logic/FeatureSource/RememberingSource.ts | 24 ++++ .../WayHandlingApplyingFeatureSource.ts | 66 +++++++++ Logic/FilteredLayer.ts | 130 ++---------------- Logic/Osm/Changes.ts | 115 ++++++++-------- State.ts | 43 +++--- UI/SimpleAddUI.ts | 13 +- assets/themes/buurtnatuur/buurtnatuur.json | 27 +--- package.json | 7 +- 17 files changed, 434 insertions(+), 265 deletions(-) rename Logic/{ => Actors}/UpdateFromOverpass.ts (89%) rename Logic/{Actors => FeatureSource}/FeatureSource.ts (52%) create mode 100644 Logic/FeatureSource/FeatureSourceMerger.ts create mode 100644 Logic/FeatureSource/FilteringFeatureSource.ts create mode 100644 Logic/FeatureSource/NoOverlapSource.ts create mode 100644 Logic/FeatureSource/RememberingSource.ts create mode 100644 Logic/FeatureSource/WayHandlingApplyingFeatureSource.ts diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index a0bfb24ea..8cd20fd44 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -145,7 +145,7 @@ export default class LayerConfig { this.title = tr("title", undefined); - this.icon = tr("icon", Img.AsData(Svg.bug)); + this.icon = tr("icon", Img.AsData(Svg.pin)); this.iconOverlays = (json.iconOverlays ?? []).map(overlay => { let tr = new TagRenderingConfig(overlay.then); if (typeof overlay.then === "string" && SharedTagRenderings.SharedIcons[overlay.then] !== undefined) { diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts index 39ea82e13..586459a7a 100644 --- a/Customizations/JSON/LayerConfigJson.ts +++ b/Customizations/JSON/LayerConfigJson.ts @@ -121,7 +121,8 @@ export interface LayerConfigJson { hideUnderlayingFeaturesMinPercentage?:number; /** - * If set, this layer will pass all the features it receives onto the next layer + * If set, this layer will pass all the features it receives onto the next layer. + * This is ideal for decoration, e.g. directionss on cameras */ passAllFeatures?:boolean diff --git a/InitUiElements.ts b/InitUiElements.ts index 2bd73ac21..066baa212 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -12,7 +12,7 @@ import State from "./State"; import {WelcomeMessage} from "./UI/WelcomeMessage"; import {LayerSelection} from "./UI/LayerSelection"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; -import UpdateFromOverpass from "./Logic/UpdateFromOverpass"; +import LoadFromOverpass from "./Logic/Actors/UpdateFromOverpass"; import {UIEventSource} from "./Logic/UIEventSource"; import {QueryParameters} from "./Logic/Web/QueryParameters"; import {PersonalLayersPanel} from "./UI/PersonalLayersPanel"; @@ -40,6 +40,12 @@ import {UserDetails} from "./Logic/Osm/OsmConnection"; import Attribution from "./UI/Misc/Attribution"; import Constants from "./Models/Constants"; import MetaTagging from "./Logic/MetaTagging"; +import FeatureSourceMerger from "./Logic/FeatureSource/FeatureSourceMerger"; +import RememberingSource from "./Logic/FeatureSource/RememberingSource"; +import FilteringFeatureSource from "./Logic/FeatureSource/FilteringFeatureSource"; +import WayHandlingApplyingFeatureSource from "./Logic/FeatureSource/WayHandlingApplyingFeatureSource"; +import FeatureSource from "./Logic/FeatureSource/FeatureSource"; +import NoOverlapSource from "./Logic/FeatureSource/NoOverlapSource"; export class InitUiElements { @@ -374,7 +380,7 @@ export class InitUiElements { const flayer: FilteredLayer = new FilteredLayer(layer, generateContents); flayers.push(flayer); - QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wehter or not layer " + layer.id + " is shown") + QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wether or not layer " + layer.id + " is shown") .map((str) => str !== "false", [], (b) => b.toString()) .syncWith( flayer.isDisplayed @@ -383,11 +389,45 @@ export class InitUiElements { State.state.filteredLayers.setData(flayers); - const updater = new UpdateFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap); + function addMatchingIds(src: FeatureSource) { + + src.features.addCallback(features => { + features.forEach(f => { + const properties = f.feature.properties; + if (properties._matching_layer_id) { + return; + } + + for (const flayer of flayers) { + if (flayer.layerDef.overpassTags.matchesProperties(properties)) { + properties._matching_layer_id = flayer.layerDef.id; + break; + } + } + }) + }); + } + + const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap); State.state.layerUpdater = updater; - updater.features.addCallback(features => { + addMatchingIds(updater); + addMatchingIds(State.state.changes); + + const source = + new FilteringFeatureSource( + flayers, + State.state.locationControl, + new FeatureSourceMerger([ + new RememberingSource(new WayHandlingApplyingFeatureSource(flayers, + new NoOverlapSource(flayers, updater) + )), + State.state.changes])); + + + source.features.addCallback((featuresFreshness: { feature: any, freshness: Date }[]) => { + let features = featuresFreshness.map(ff => ff.feature); features.forEach(feature => { State.state.allElements.addElement(feature); }) @@ -402,13 +442,10 @@ export class InitUiElements { } return; } - // We use window.setTimeout to give JS some time to update everything and make the interface not too laggy - window.setTimeout(() => { - const layer = layers[0]; - const rest = layers.slice(1, layers.length); - features = layer.SetApplicableData(features); - renderLayers(rest); - }, 50) + const layer = layers[0]; + const rest = layers.slice(1, layers.length); + features = layer.SetApplicableData(features); + renderLayers(rest); } renderLayers(flayers); diff --git a/Logic/Actors/StrayClickHandler.ts b/Logic/Actors/StrayClickHandler.ts index 2445389ee..558a0d227 100644 --- a/Logic/Actors/StrayClickHandler.ts +++ b/Logic/Actors/StrayClickHandler.ts @@ -2,7 +2,6 @@ import * as L from "leaflet"; import {UIElement} from "../../UI/UIElement"; import Svg from "../../Svg"; import {UIEventSource} from "../UIEventSource"; -import {FilteredLayer} from "../FilteredLayer"; import Img from "../../UI/Base/Img"; /** @@ -16,7 +15,7 @@ export class StrayClickHandler { constructor( lastClickLocation: UIEventSource<{ lat: number, lon:number }>, selectedElement: UIEventSource, - filteredLayers: UIEventSource, + filteredLayers: UIEventSource<{ readonly isDisplayed: UIEventSource}[]>, leafletMap: UIEventSource, fullscreenMessage: UIEventSource, uiToShow: (() => UIElement)) { diff --git a/Logic/UpdateFromOverpass.ts b/Logic/Actors/UpdateFromOverpass.ts similarity index 89% rename from Logic/UpdateFromOverpass.ts rename to Logic/Actors/UpdateFromOverpass.ts index 9b6ff9a7b..ab4e95b10 100644 --- a/Logic/UpdateFromOverpass.ts +++ b/Logic/Actors/UpdateFromOverpass.ts @@ -1,22 +1,19 @@ -import {Or, TagsFilter} from "./Tags"; -import {UIEventSource} from "./UIEventSource"; -import Bounds from "../Models/Bounds"; -import {Overpass} from "./Osm/Overpass"; -import Loc from "../Models/Loc"; -import LayoutConfig from "../Customizations/JSON/LayoutConfig"; -import FeatureSource from "./Actors/FeatureSource"; +import {UIEventSource} from "../UIEventSource"; +import Loc from "../../Models/Loc"; +import {Or, TagsFilter} from "../Tags"; +import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; +import {Overpass} from "../Osm/Overpass"; +import Bounds from "../../Models/Bounds"; +import FeatureSource from "../FeatureSource/FeatureSource"; + export default class UpdateFromOverpass implements FeatureSource{ /** * The last loaded features of the geojson */ - public readonly features: UIEventSource = new UIEventSource(undefined); + public readonly features: UIEventSource<{feature:any, freshness: Date}[]> = new UIEventSource(undefined); - /** - * The time of updating according to Overpass - */ - public readonly freshness:UIEventSource = new UIEventSource(undefined); public readonly sufficientlyZoomed: UIEventSource; public readonly runningQuery: UIEventSource = new UIEventSource(false); @@ -142,8 +139,7 @@ export default class UpdateFromOverpass implements FeatureSource{ function (data, date) { self._previousBounds.get(z).push(queryBounds); self.retries.setData(0); - self.freshness.setData(date); - self.features.setData(data.features); + self.features.setData(data.features.map(f => ({feature: f, freshness: date}))); self.runningQuery.setData(false); }, function (reason) { diff --git a/Logic/Actors/FeatureSource.ts b/Logic/FeatureSource/FeatureSource.ts similarity index 52% rename from Logic/Actors/FeatureSource.ts rename to Logic/FeatureSource/FeatureSource.ts index 1288bceb7..16475b950 100644 --- a/Logic/Actors/FeatureSource.ts +++ b/Logic/FeatureSource/FeatureSource.ts @@ -1,8 +1,5 @@ import {UIEventSource} from "../UIEventSource"; export default interface FeatureSource { - - features : UIEventSource; - freshness: UIEventSource; - + features: UIEventSource<{feature: any, freshness: Date}[]>; } \ No newline at end of file diff --git a/Logic/FeatureSource/FeatureSourceMerger.ts b/Logic/FeatureSource/FeatureSourceMerger.ts new file mode 100644 index 000000000..1755aef17 --- /dev/null +++ b/Logic/FeatureSource/FeatureSourceMerger.ts @@ -0,0 +1,40 @@ +import FeatureSource from "./FeatureSource"; +import {UIEventSource} from "../UIEventSource"; + +export default class FeatureSourceMerger implements FeatureSource { + + public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{feature: any; freshness: Date}[]>([]); + private readonly _sources: FeatureSource[]; + + constructor(sources: FeatureSource[]) { + this._sources = sources; + const self = this; + for (const source of sources) { + source.features.addCallback(() => self.Update()); + } + } + + private Update() { + let all = {}; // Mapping 'id' -> {feature, freshness} + for (const source of this._sources) { + for (const f of source.features.data) { + const id = f.feature.properties.id+f.feature.geometry.type; + const oldV = all[id]; + if(oldV === undefined){ + all[id] = f; + }else{ + if(oldV.freshness < f.freshness){ + all[id]=f; + } + } + } + } + const newList = []; + for (const id in all) { + newList.push(all[id]); + } + this.features.setData(newList); + } + + +} \ No newline at end of file diff --git a/Logic/FeatureSource/FilteringFeatureSource.ts b/Logic/FeatureSource/FilteringFeatureSource.ts new file mode 100644 index 000000000..f3317aa90 --- /dev/null +++ b/Logic/FeatureSource/FilteringFeatureSource.ts @@ -0,0 +1,52 @@ +import FeatureSource from "./FeatureSource"; +import {UIEventSource} from "../UIEventSource"; +import LayerConfig from "../../Customizations/JSON/LayerConfig"; +import Loc from "../../Models/Loc"; + +export default class FilteringFeatureSource implements FeatureSource { + public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); + + constructor(layers: { + isDisplayed: UIEventSource, + layerDef: LayerConfig + }[], + location: UIEventSource, + upstream: FeatureSource) { + + const layerDict = {}; + + const self = this; + + function update() { + console.log("UPdating...") + const features: { feature: any, freshness: Date }[] = upstream.features.data; + const newFeatures = features.filter(f => { + const layerId = f.feature.properties._matching_layer_id; + if (layerId === undefined) { + console.error(f) + throw "feature._matching_layer_id is undefined" + } + const layer: { + isDisplayed: UIEventSource, + layerDef: LayerConfig + } = layerDict[layerId]; + if (layer === undefined) { + throw "No layer found with id " + layerId; + } + return layer.isDisplayed.data && (layer.layerDef.minzoom <= location.data.zoom); + }); + self.features.setData(newFeatures); + } + for (const layer of layers) { + layerDict[layer.layerDef.id] = layer; + layer.isDisplayed.addCallback(update) + } + upstream.features.addCallback(update); + location.map(l => l.zoom).addCallback(update); + + + } + + + +} \ No newline at end of file diff --git a/Logic/FeatureSource/NoOverlapSource.ts b/Logic/FeatureSource/NoOverlapSource.ts new file mode 100644 index 000000000..9dd66810d --- /dev/null +++ b/Logic/FeatureSource/NoOverlapSource.ts @@ -0,0 +1,86 @@ +import LayerConfig from "../../Customizations/JSON/LayerConfig"; +import FeatureSource from "./FeatureSource"; +import {UIEventSource} from "../UIEventSource"; +import {GeoOperations} from "../GeoOperations"; + +/** + * The no overlap source takes a featureSource and applies a filter on it. + * First, it'll figure out for each feature to which layer it belongs + * Then, it'll check any feature of any 'lower' layer + */ +export default class NoOverlapSource { + + features: UIEventSource<{ feature: any, freshness: Date }[]> = new UIEventSource<{ feature: any, freshness: Date }[]>([]); + + constructor(layers: { + layerDef: LayerConfig + }[], + upstream: FeatureSource) { + const layerDict = {}; + let noOverlapRemoval = true; + const layerIds = [] + for (const layer of layers) { + layerDict[layer.layerDef.id] = layer; + layerIds.push(layer.layerDef.id); + if ((layer.layerDef.hideUnderlayingFeaturesMinPercentage ?? 0) !== 0) { + noOverlapRemoval = false; + } + } + if (noOverlapRemoval) { + this.features = upstream.features; + return; + } + + + this.features = upstream.features.map( + features => { + if (features === undefined) { + return; + } + + // There is overlap removal active + // We partition all the features with their respective layerIDs + const partitions = {}; + for (const layerId of layerIds) { + partitions[layerId] = [] + } + for (const feature of features) { + partitions[feature.feature.properties._matching_layer_id].push(feature); + } + + // With this partitioning in hand, we run over every layer and remove every underlying feature if needed + for (let i = 0; i < layerIds.length; i++) { + let layerId = layerIds[i]; + const percentage = layerDict[layerId].layerDef.hideUnderlayingFeaturesMinPercentage ?? 0; + if (percentage === 0) { + // We don't have to remove underlying features! + continue; + } + const guardPartition = partitions[layerId]; + for (let j = i + 1; j < layerIds.length; j++) { + let layerJd = layerIds[j]; + let partitionToShrink: { feature: any, freshness: Date }[] = partitions[layerJd]; + let newPartition = []; + for (const mightBeDeleted of partitionToShrink) { + const doesOverlap = GeoOperations.featureIsContainedInAny( + mightBeDeleted.feature, + guardPartition.map(f => f.feature), + percentage + ); + if(!doesOverlap){ + newPartition.push(mightBeDeleted); + } + } + partitions[layerJd] = newPartition; + } + } + + // At last, we create the actual new features + let newFeatures: { feature: any, freshness: Date }[] = []; + for (const layerId of layerIds) { + newFeatures = newFeatures.concat(partitions[layerId]); + } + return newFeatures; + }); + } +} \ No newline at end of file diff --git a/Logic/FeatureSource/RememberingSource.ts b/Logic/FeatureSource/RememberingSource.ts new file mode 100644 index 000000000..58628dfc3 --- /dev/null +++ b/Logic/FeatureSource/RememberingSource.ts @@ -0,0 +1,24 @@ +/** + * Every previously added point is remembered, but new points are added + */ +import FeatureSource from "./FeatureSource"; +import {UIEventSource} from "../UIEventSource"; + +export default class RememberingSource implements FeatureSource{ + features: UIEventSource<{feature: any, freshness: Date}[]> = new UIEventSource<{feature: any, freshness: Date}[]>([]); + + constructor(source: FeatureSource) { + const self = this; + source.features.addCallbackAndRun(features => { + if(features === undefined){ + return; + } + const ids = new Set( features.map(f => f.feature.properties.id+f.feature.geometry.type)); + const newList = features.concat( + self.features.data.filter(old => !ids.has(old.feature.properties.id+old.feature.geometry.type)) + ) + self.features.setData(newList); + }) + } + +} \ No newline at end of file diff --git a/Logic/FeatureSource/WayHandlingApplyingFeatureSource.ts b/Logic/FeatureSource/WayHandlingApplyingFeatureSource.ts new file mode 100644 index 000000000..170e434b6 --- /dev/null +++ b/Logic/FeatureSource/WayHandlingApplyingFeatureSource.ts @@ -0,0 +1,66 @@ +import FeatureSource from "./FeatureSource"; +import {UIEventSource} from "../UIEventSource"; +import LayerConfig from "../../Customizations/JSON/LayerConfig"; +import {GeoOperations} from "../GeoOperations"; + +export default class WayHandlingApplyingFeatureSource implements FeatureSource { + features: UIEventSource<{ feature: any; freshness: Date }[]>; + + constructor(layers: { + layerDef: LayerConfig + }[], + upstream: FeatureSource) { + const layerDict = {}; + let allDefaultWayHandling = true; + for (const layer of layers) { + layerDict[layer.layerDef.id] = layer; + if (layer.layerDef.wayHandling !== LayerConfig.WAYHANDLING_DEFAULT) { + allDefaultWayHandling = false; + } + } + if (allDefaultWayHandling) { + this.features = upstream.features; + return; + } + + + this.features = upstream.features.map( + features => { + if(features === undefined){ + return; + } + const newFeatures: { feature: any, freshness: Date }[] = []; + for (const f of features) { + const feat = f.feature; + const layerId = feat.properties._matching_layer_id; + const layer: LayerConfig = layerDict[layerId].layerDef; + if (layer === undefined) { + throw "No layer found with id " + layerId; + } + + if(layer.wayHandling === LayerConfig.WAYHANDLING_DEFAULT){ + newFeatures.push(f); + continue; + } + + if (feat.geometry.type === "Point") { + newFeatures.push(f); + // it is a point, nothing to do here + continue; + } + + const centerPoint = GeoOperations.centerpoint(feat); + newFeatures.push({feature: centerPoint, freshness: f.freshness}); + + if(layer.wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY){ + newFeatures.push(f); + } + + } + return newFeatures; + } + ); + + } + +} \ No newline at end of file diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts index 86ec12501..ed2f9b06c 100644 --- a/Logic/FilteredLayer.ts +++ b/Logic/FilteredLayer.ts @@ -10,29 +10,20 @@ import Hash from "./Web/Hash"; import LazyElement from "../UI/Base/LazyElement"; /*** - * A filtered layer is a layer which offers a 'set-data' function - * It is initialized with a tagfilter. - * - * When geojson-data is given to 'setData', all the geojson matching the filter, is rendered on this layer. - * If it is not rendered, it is returned in a 'leftOver'-geojson; which can be consumed by the next layer. - * - * This also makes sure that no objects are rendered twice if they are applicable on two layers + * */ export class FilteredLayer { public readonly name: string | UIElement; - public readonly filters: TagsFilter; public readonly isDisplayed: UIEventSource = new UIEventSource(true); public readonly layerDef: LayerConfig; - private readonly combinedIsDisplayed: UIEventSource; + + private readonly filters: TagsFilter; private readonly _maxAllowedOverlap: number; /** The featurecollection from overpass */ private _dataFromOverpass: any[]; - /** List of new elements, geojson features - */ - private _newElements = []; /** * The leaflet layer object which should be removed on rerendering */ @@ -51,22 +42,7 @@ export class FilteredLayer { this.name = name; this.filters = layerDef.overpassTags; this._maxAllowedOverlap = layerDef.hideUnderlayingFeaturesMinPercentage; - const self = this; - this.combinedIsDisplayed = this.isDisplayed.map(isDisplayed => { - return isDisplayed && State.state.locationControl.data.zoom >= self.layerDef.minzoom - }, - [State.state.locationControl] - ); - this.combinedIsDisplayed.addCallback(function (isDisplayed) { - const map = State.state.leafletMap.data; - if (self._geolayer !== undefined && self._geolayer !== null) { - if (isDisplayed) { - self._geolayer.addTo(map); - } else { - map.removeLayer(self._geolayer); - } - } - }) + } /** @@ -88,29 +64,11 @@ export class FilteredLayer { } this.RenderLayer(selfFeatures) - - const notShadowed = []; - for (const feature of leftoverFeatures) { - if (this._maxAllowedOverlap !== undefined && this._maxAllowedOverlap > 0) { - if (GeoOperations.featureIsContainedInAny(feature, selfFeatures, this._maxAllowedOverlap)) { - // This feature is filtered away - continue; - } - } - - notShadowed.push(feature); - } - - return notShadowed; + return leftoverFeatures; } - public AddNewElement(element) { - this._newElements.push(element); - this.RenderLayer(this._dataFromOverpass); // Update the layer - } - - private RenderLayer(features) { + private RenderLayer(features: any[]) { if (this._geolayer !== undefined && this._geolayer !== null) { // Remove the old geojson layer from the map - we'll reshow all the elements later on anyway @@ -118,12 +76,9 @@ export class FilteredLayer { } // We fetch all the data we have to show: - let fusedFeatures = this.ApplyWayHandling(this.FuseData(features)); - - // And we copy some features as points - if needed const data = { type: "FeatureCollection", - features: fusedFeatures + features: features } let self = this; @@ -144,13 +99,7 @@ export class FilteredLayer { radius: 25, color: style.color }); - } else if (style.icon.iconUrl.startsWith("$circle")) { - marker = L.circle(latLng, { - radius: 25, - color: style.color - }); } else { - style.icon.html.ListenTo(self.isDisplayed) marker = L.marker(latLng, { icon: L.divIcon({ html: style.icon.html.Render(), @@ -206,72 +155,9 @@ export class FilteredLayer { } }); - if (this.combinedIsDisplayed.data) { - this._geolayer.addTo(State.state.leafletMap.data); - } + this._geolayer.addTo(State.state.leafletMap.data); } - private ApplyWayHandling(fusedFeatures: any[]) { - if (this.layerDef.wayHandling === LayerConfig.WAYHANDLING_DEFAULT) { - // We don't have to do anything special - return fusedFeatures; - } - - // We have to convert all the ways into centerpoints - const existingPoints = []; - const newPoints = []; - const existingWays = []; - - for (const feature of fusedFeatures) { - if (feature.geometry.type === "Point") { - existingPoints.push(feature); - continue; - } - - existingWays.push(feature); - const centerPoint = GeoOperations.centerpoint(feature); - newPoints.push(centerPoint); - } - - fusedFeatures = existingPoints.concat(newPoints); - if (this.layerDef.wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY) { - fusedFeatures = fusedFeatures.concat(existingWays) - } - return fusedFeatures; - } - - //*Fuses the old and the new datasets*/ - private FuseData(data: any[]) { - const oldData = this._dataFromOverpass ?? []; - - // We keep track of all the ids that are freshly loaded in order to avoid adding duplicates - const idsFromOverpass: Set = new Set(); - // A list of all the features to show - const fusedFeatures = []; - // First, we add all the fresh data: - for (const feature of data) { - idsFromOverpass.add(feature.properties.id); - fusedFeatures.push(feature); - } - // Now we add all the stale data - for (const feature of oldData) { - if (idsFromOverpass.has(feature.properties.id)) { - continue; // Feature already loaded and a fresher version is available - } - idsFromOverpass.add(feature.properties.id); - fusedFeatures.push(feature); - } - this._dataFromOverpass = fusedFeatures; - - for (const feature of this._newElements) { - if (!idsFromOverpass.has(feature.properties.id)) { - // This element is not yet uploaded or not yet visible in overpass - // We include it in the layer - fusedFeatures.push(feature); - } - } - return fusedFeatures; - } } \ No newline at end of file diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index fc4a60655..6aa514564 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -1,71 +1,25 @@ -/** - * Handles all changes made to OSM. - * Needs an authenticator via OsmConnection - */ import {OsmNode, OsmObject} from "./OsmObject"; import {And, Tag, TagsFilter} from "../Tags"; import State from "../../State"; import {Utils} from "../../Utils"; import {UIEventSource} from "../UIEventSource"; import Constants from "../../Models/Constants"; +import FeatureSource from "../FeatureSource/FeatureSource"; -export class Changes { +/** + * Handles all changes made to OSM. + * Needs an authenticator via OsmConnection + */ +export class Changes implements FeatureSource{ - private static _nextId = -1; // New assined ID's are negative - - addTag(elementId: string, tagsFilter: TagsFilter, - tags?: UIEventSource) { - const changes = this.tagToChange(tagsFilter); - if (changes.length == 0) { - return; - } - const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); - const elementTags = eventSource.data; - const pending : {elementId:string, key: string, value: string}[] = []; - for (const change of changes) { - if (elementTags[change.k] !== change.v) { - elementTags[change.k] = change.v; - pending.push({elementId: elementTags.id, key: change.k, value: change.v}); - } - } - if(pending.length === 0){ - return; - } - console.log("Sending ping",eventSource) - eventSource.ping(); - this.uploadAll([], pending); - } - - - private tagToChange(tagsFilter: TagsFilter) { - let changes: { k: string, v: string }[] = []; - - if (tagsFilter instanceof Tag) { - const tag = tagsFilter as Tag; - if (typeof tag.value !== "string") { - throw "Invalid value" - } - return [this.checkChange(tag.key, tag.value)]; - } - - if (tagsFilter instanceof And) { - const and = tagsFilter as And; - for (const tag of and.and) { - changes = changes.concat(this.tagToChange(tag)); - } - return changes; - } - console.log("Unsupported tagsfilter element to addTag", tagsFilter); - throw "Unsupported tagsFilter element"; - } + public features = new UIEventSource<{feature: any, freshness: Date}[]>([]); + + private static _nextId = -1; // Newly assigned ID's are negative /** * Adds a change to the pending changes - * @param elementId - * @param key - * @param value */ - private checkChange(key: string, value: string): { k: string, v: string } { + private static checkChange(key: string, value: string): { k: string, v: string } { if (key === undefined || key === null) { console.log("Invalid key"); return undefined; @@ -85,12 +39,35 @@ export class Changes { return {k: key, v: value}; } + addTag(elementId: string, tagsFilter: TagsFilter, + tags?: UIEventSource) { + const changes = this.tagToChange(tagsFilter); + if (changes.length == 0) { + return; + } + const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); + const elementTags = eventSource.data; + const pending: { elementId: string, key: string, value: string }[] = []; + for (const change of changes) { + if (elementTags[change.k] !== change.v) { + elementTags[change.k] = change.v; + pending.push({elementId: elementTags.id, key: change.k, value: change.v}); + } + } + if (pending.length === 0) { + return; + } + console.log("Sending ping", eventSource) + eventSource.ping(); + this.uploadAll([], pending); + } + /** * Create a new node element at the given lat/long. * An internal OsmObject is created to upload later on, a geojson represention is returned. * Note that the geojson version shares the tags (properties) by pointer, but has _no_ id in properties */ - createElement(basicTags:Tag[], lat: number, lon: number) { + public createElement(basicTags: Tag[], lat: number, lon: number) { console.log("Creating a new element with ", basicTags) const osmNode = new OsmNode(Changes._nextId); Changes._nextId--; @@ -113,6 +90,9 @@ export class Changes { } } + this.features.data.push({feature:geojson, freshness: new Date()}); + this.features.ping(); + // The basictags are COPIED, the id is included in the properties // The tags are not yet written into the OsmObject, but this is applied onto a const changes = []; @@ -128,6 +108,27 @@ export class Changes { return geojson; } + private tagToChange(tagsFilter: TagsFilter) { + let changes: { k: string, v: string }[] = []; + + if (tagsFilter instanceof Tag) { + const tag = tagsFilter as Tag; + if (typeof tag.value !== "string") { + throw "Invalid value" + } + return [Changes.checkChange(tag.key, tag.value)]; + } + + if (tagsFilter instanceof And) { + const and = tagsFilter as And; + for (const tag of and.and) { + changes = changes.concat(this.tagToChange(tag)); + } + return changes; + } + console.log("Unsupported tagsfilter element to addTag", tagsFilter); + throw "Unsupported tagsFilter element"; + } private uploadChangesWithLatestVersions( knownElements, newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) { diff --git a/State.ts b/State.ts index 0cb5d363a..0ca0ded6e 100644 --- a/State.ts +++ b/State.ts @@ -5,8 +5,6 @@ import {Changes} from "./Logic/Osm/Changes"; import {OsmConnection} from "./Logic/Osm/OsmConnection"; import Locale from "./UI/i18n/Locale"; import Translations from "./UI/i18n/Translations"; -import {FilteredLayer} from "./Logic/FilteredLayer"; -import {UpdateFromOverpass} from "./Logic/UpdateFromOverpass"; import {UIEventSource} from "./Logic/UIEventSource"; import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; import {QueryParameters} from "./Logic/Web/QueryParameters"; @@ -20,6 +18,8 @@ import Constants from "./Models/Constants"; import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; import * as L from "leaflet" import LayerResetter from "./Logic/Actors/LayerResetter"; +import UpdateFromOverpass from "./Logic/Actors/UpdateFromOverpass"; +import LayerConfig from "./Customizations/JSON/LayerConfig"; /** * Contains the global state: a bunch of UI-event sources @@ -29,9 +29,9 @@ export default class State { // The singleton of the global state public static state: State; - + public static runningFromConsole: boolean = false; - + public readonly layoutToUse = new UIEventSource(undefined); /** @@ -62,7 +62,15 @@ export default class State { public layerUpdater: UpdateFromOverpass; - public filteredLayers: UIEventSource = new UIEventSource([]) + public filteredLayers: UIEventSource<{ + readonly name: string | UIElement; + readonly isDisplayed: UIEventSource, + readonly layerDef: LayerConfig; + }[]> = new UIEventSource<{ + readonly name: string | UIElement; + readonly isDisplayed: UIEventSource, + readonly layerDef: LayerConfig; + }[]>([]) /** * The message that should be shown at the center of the screen @@ -102,9 +110,9 @@ export default class State { * The location as delivered by the GPS */ public currentGPSLocation: UIEventSource<{ - latlng: {lat:number, lng:number}, + latlng: { lat: number, lng: number }, accuracy: number - }> = new UIEventSource<{ latlng: {lat:number, lng:number}, accuracy: number }>(undefined); + }> = new UIEventSource<{ latlng: { lat: number, lng: number }, accuracy: number }>(undefined); public layoutDefinition: string; public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; @@ -121,7 +129,7 @@ export default class State { const zoom = State.asFloat( QueryParameters.GetQueryParameter("z", "" + layoutToUse.startZoom, "The initial/current zoom level") - .syncWith(LocalStorageSource.Get("zoom"))); + .syncWith(LocalStorageSource.Get("zoom"))); const lat = State.asFloat(QueryParameters.GetQueryParameter("lat", "" + layoutToUse.startLat, "The initial/current latitude") .syncWith(LocalStorageSource.Get("lat"))); const lon = State.asFloat(QueryParameters.GetQueryParameter("lon", "" + layoutToUse.startLon, "The initial/current longitude of the app") @@ -163,11 +171,8 @@ export default class State { new LayerResetter( - this.backgroundLayer,this.locationControl, - this.availableBackgroundLayers, this.layoutToUse.map((layout : LayoutConfig)=> layout.defaultBackgroundId)); - - - + this.backgroundLayer, this.locationControl, + this.availableBackgroundLayers, this.layoutToUse.map((layout: LayoutConfig) => layout.defaultBackgroundId)); function featSw(key: string, deflt: (layout: LayoutConfig) => boolean, documentation: string): UIEventSource { @@ -192,7 +197,7 @@ export default class State { "Disables/Enables the layer control"); this.featureSwitchAddNew = featSw("fs-add-new", (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true, "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)"); - this.featureSwitchWelcomeMessage = featSw("fs-welcome-message", () => true, + this.featureSwitchWelcomeMessage = featSw("fs-welcome-message", () => true, "Disables/enables the help menu or welcome message"); this.featureSwitchIframe = featSw("fs-iframe", () => false, "Disables/Enables the iframe-popup"); @@ -204,8 +209,6 @@ export default class State { "Disables/Enables the geolocation button"); - - const testParam = QueryParameters.GetQueryParameter("test", "false", "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org").data; this.osmConnection = new OsmConnection( @@ -231,8 +234,8 @@ export default class State { } ) h.addCallbackAndRun(hash => { - if(hash === undefined || hash === ""){ - self.selectedElement.setData(undefined); + if (hash === undefined || hash === "") { + self.selectedElement.setData(undefined); } }) @@ -284,7 +287,7 @@ export default class State { } - private static asFloat(source: UIEventSource): UIEventSource { + private static asFloat(source: UIEventSource): UIEventSource { return source.map(str => { let parsed = parseFloat(str); return isNaN(parsed) ? undefined : parsed; @@ -295,5 +298,5 @@ export default class State { return ("" + fl).substr(0, 8); }) } - + } diff --git a/UI/SimpleAddUI.ts b/UI/SimpleAddUI.ts index c8a92bef1..70e4aafc7 100644 --- a/UI/SimpleAddUI.ts +++ b/UI/SimpleAddUI.ts @@ -1,6 +1,5 @@ import {UIElement} from "./UIElement"; import {Tag, TagUtils} from "../Logic/Tags"; -import {FilteredLayer} from "../Logic/FilteredLayer"; import Translations from "./i18n/Translations"; import Combine from "./Base/Combine"; import {SubtleButton} from "./Base/SubtleButton"; @@ -25,7 +24,9 @@ export class SimpleAddUI extends UIElement { name: string | UIElement, icon: UIElement, tags: Tag[], - layerToAddTo: FilteredLayer + layerToAddTo: { + name: UIElement | string, + isDisplayed: UIEventSource } }> = new UIEventSource(undefined); private confirmButton: UIElement = undefined; @@ -81,7 +82,7 @@ export class SimpleAddUI extends UIElement { "", Translations.t.general.add.confirmButton.Subs({category: preset.title}), ""])); - self.confirmButton.onClick(self.CreatePoint(preset.tags, layer)); + self.confirmButton.onClick(self.CreatePoint(preset.tags)); self._confirmDescription = preset.description; self._confirmPreset.setData({ tags: preset.tags, @@ -112,13 +113,11 @@ export class SimpleAddUI extends UIElement { }) } - private CreatePoint(tags: Tag[], layerToAddTo: FilteredLayer) { + private CreatePoint(tags: Tag[]) { return () => { - const loc = State.state.LastClickLocation.data; let feature = State.state.changes.createElement(tags, loc.lat, loc.lon); State.state.selectedElement.setData(feature); - layerToAddTo.AddNewElement(feature); } } @@ -130,7 +129,7 @@ export class SimpleAddUI extends UIElement { if(!this._confirmPreset.data.layerToAddTo.isDisplayed.data){ return new Combine([ - Translations.t.general.add.layerNotEnabled.Subs({layer: this._confirmPreset.data.layerToAddTo.layerDef.name}) + Translations.t.general.add.layerNotEnabled.Subs({layer: this._confirmPreset.data.layerToAddTo.name}) .SetClass("alert"), this.openLayerControl, diff --git a/assets/themes/buurtnatuur/buurtnatuur.json b/assets/themes/buurtnatuur/buurtnatuur.json index 3a6fed45f..692d3d75a 100644 --- a/assets/themes/buurtnatuur/buurtnatuur.json +++ b/assets/themes/buurtnatuur/buurtnatuur.json @@ -75,14 +75,7 @@ ], "hideUnderlayingFeaturesMinPercentage": 10, "icon": { - "render": "./assets/themes/buurtnatuur/nature_reserve.svg", - "mappings": [ - { - "#": "This is a little bit a hack to force a circle to be shown while keeping the icon in the 'new' menu", - "if": "id~node/[0-9]*", - "then": "$circle" - } - ] + "render": "circle:#ffffff;./assets/themes/buurtnatuur/nature_reserve.svg" }, "width": { "render": "5" @@ -179,14 +172,7 @@ ], "hideUnderlayingFeaturesMinPercentage": 10, "icon": { - "render": "./assets/themes/buurtnatuur/park.svg", - "mappings": [ - { - "#": "This is a little bit a hack to force a circle to be shown while keeping the icon in the 'new' menu", - "if": "id~node/[0-9]*", - "then": "$circle" - } - ] + "render": "circle:#ffffff;./assets/themes/buurtnatuur/park.svg" }, "width": { "render": "5" @@ -271,14 +257,7 @@ ], "hideUnderlayingFeaturesMinPercentage": 0, "icon": { - "render": "./assets/themes/buurtnatuur/forest.svg", - "mappings": [ - { - "#": "This is a little bit a hack to force a circle to be shown while keeping the icon in the 'new' menu", - "if": "id~node/[0-9]*", - "then": "$circle" - } - ] + "render": "circle:#ffffff;./assets/themes/buurtnatuur/forest.svg" }, "width": { "render": "5" diff --git a/package.json b/package.json index deaf15a4e..dada662e0 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,12 @@ "version": "0.0.5", "repository": "https://github.com/pietervdvn/MapComplete", "description": "A small website to edit OSM easily", + "bugs": "https://github.com/pietervdvn/MapComplete/issues", + "homepage": "https://mapcomplete.osm.be", "main": "index.js", "scripts": { - "start": "parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", + "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", + "start": "npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", "test": "ts-node test/*", "generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json", "generate:images": "ts-node scripts/generateIncludedImages.ts", @@ -24,7 +27,7 @@ "Editor" ], "author": "pietervdvn", - "license": "MIT", + "license": "GPL", "dependencies": { "@types/leaflet-providers": "^1.2.0", "country-language": "^0.1.7",