From 9a5b35b9f30f8216ab7df8740cbb410facd734d6 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 28 Aug 2020 03:16:21 +0200 Subject: [PATCH] More work on cyclestreet layout, add loading of layers depending on zoom level --- Customizations/AllKnownLayouts.ts | 2 + Customizations/JSON/CustomLayoutFromJSON.ts | 1 + Customizations/LayerDefinition.ts | 1 + Customizations/Layouts/Cyclofix.ts | 2 +- InitUiElements.ts | 4 -- Logic/FilteredLayer.ts | 28 ++++++-- Logic/ImageSearcher.ts | 3 +- Logic/LayerUpdater.ts | 67 +++++++++++++------- Logic/Leaflet/Basemap.ts | 6 ++ Logic/Osm/OsmConnection.ts | 4 +- Logic/Osm/OsmImageUploadHandler.ts | 3 +- Logic/PersonalLayersPanel.ts | 2 +- State.ts | 8 +-- UI/MoreScreen.ts | 12 ++-- UI/i18n/Translations.ts | 7 +- assets/themes/cyclestreets/cyclestreets.json | 12 ++-- index.ts | 6 +- 17 files changed, 109 insertions(+), 59 deletions(-) diff --git a/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts index 8d428470be..6c406050af 100644 --- a/Customizations/AllKnownLayouts.ts +++ b/Customizations/AllKnownLayouts.ts @@ -57,6 +57,7 @@ export class AllKnownLayouts { continue; } this.allLayers[layer.id] = layer; + this.allLayers[layer.id.toLowerCase()] = layer; all.layers.push(layer); } } @@ -64,6 +65,7 @@ export class AllKnownLayouts { const allSets: Map = new Map(); for (const layout of this.layoutsList) { allSets[layout.name] = layout; + allSets[layout.name.toLowerCase()] = layout; } allSets[all.name] = all; return allSets; diff --git a/Customizations/JSON/CustomLayoutFromJSON.ts b/Customizations/JSON/CustomLayoutFromJSON.ts index 584ecd5b53..6a6e415888 100644 --- a/Customizations/JSON/CustomLayoutFromJSON.ts +++ b/Customizations/JSON/CustomLayoutFromJSON.ts @@ -45,6 +45,7 @@ export interface LayerConfigJson { width?: TagRenderingConfigJson; overpassTags: string | { k: string, v: string }[]; wayHandling?: number, + widenFactor?: number, presets: { tags: string, title: string | any, diff --git a/Customizations/LayerDefinition.ts b/Customizations/LayerDefinition.ts index 34db35c437..bf876aba18 100644 --- a/Customizations/LayerDefinition.ts +++ b/Customizations/LayerDefinition.ts @@ -106,6 +106,7 @@ export class LayerDefinition { elementsToShow?: TagDependantUIElementConstructor[], maxAllowedOverlapPercentage?: number, wayHandling?: number, + widenFactor?: number, style?: (tags: any) => { color: string, icon: any diff --git a/Customizations/Layouts/Cyclofix.ts b/Customizations/Layouts/Cyclofix.ts index 7282257664..4e5afb6652 100644 --- a/Customizations/Layouts/Cyclofix.ts +++ b/Customizations/Layouts/Cyclofix.ts @@ -13,7 +13,7 @@ export default class Cyclofix extends Layout { constructor() { super( "cyclofix", - ["en", "nl", "fr"], + ["en", "nl", "fr","gl"], Translations.t.cyclofix.title, [new BikeServices(), new BikeShops(), new DrinkingWater(), new BikeParkings(), new BikeOtherShops(), new BikeCafes()], 16, diff --git a/InitUiElements.ts b/InitUiElements.ts index b4a1ffd546..61f09ce634 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -183,7 +183,6 @@ export class InitUiElements { const flayers: FilteredLayer[] = [] const presets: Preset[] = []; - let minZoom = 0; const state = State.state; for (const layer of state.layoutToUse.data.layers) { @@ -197,9 +196,6 @@ export class InitUiElements { ) }; - minZoom = Math.max(minZoom, layer.minzoom); - - for (const preset of layer.presets ?? []) { if (preset.icon === undefined) { diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts index 34c2603973..d5e51814b7 100644 --- a/Logic/FilteredLayer.ts +++ b/Logic/FilteredLayer.ts @@ -29,7 +29,7 @@ export class FilteredLayer { /** The featurecollection from overpass */ - private _dataFromOverpass; + private _dataFromOverpass : any[]; private _wayHandling: number; /** List of new elements, geojson features */ @@ -146,7 +146,7 @@ export class FilteredLayer { public AddNewElement(element) { this._newElements.push(element); console.log("Element added"); - this.RenderLayer(this._dataFromOverpass); // Update the layer + this.RenderLayer({features:this._dataFromOverpass}); // Update the layer } @@ -154,23 +154,39 @@ export class FilteredLayer { let self = this; if (this._geolayer !== undefined && this._geolayer !== null) { + // Remove the old geojson layer from the map - we'll reshow all the elements later on anyway State.state.bm.map.removeLayer(this._geolayer); } - this._dataFromOverpass = data; + + 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 = []; - const idsFromOverpass = []; + // First, we add all the fresh data: for (const feature of data.features) { - idsFromOverpass.push(feature.properties.id); + 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); } for (const feature of this._newElements) { - if (idsFromOverpass.indexOf(feature.properties.id) < 0) { + 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); } } + + this._dataFromOverpass = fusedFeatures; // We use a new, fused dataset data = { diff --git a/Logic/ImageSearcher.ts b/Logic/ImageSearcher.ts index 6ab99e5d7e..f79b31d154 100644 --- a/Logic/ImageSearcher.ts +++ b/Logic/ImageSearcher.ts @@ -6,6 +6,7 @@ import {ImgurImage} from "../UI/Image/ImgurImage"; import {State} from "../State"; import {ImagesInCategory, Wikidata, Wikimedia} from "./Web/Wikimedia"; import {UIEventSource} from "./UIEventSource"; +import {Tag} from "./TagsFilter"; /** * There are multiple way to fetch images for an object @@ -121,7 +122,7 @@ export class ImageSearcher extends UIEventSource { return; } console.log("Deleting image...", key, " --> ", url); - State.state.changes.addChange(this._tags.data.id, key, ""); + State.state.changes.addTag(this._tags.data.id, new Tag(key, "")); this._deletedImages.data.push(url); this._deletedImages.ping(); } diff --git a/Logic/LayerUpdater.ts b/Logic/LayerUpdater.ts index e3c3f09ed3..a89ff4edf3 100644 --- a/Logic/LayerUpdater.ts +++ b/Logic/LayerUpdater.ts @@ -8,13 +8,17 @@ import {State} from "../State"; export class LayerUpdater { - public readonly sufficentlyZoomed: UIEventSource = new UIEventSource(false); + public readonly sufficentlyZoomed: UIEventSource; public readonly runningQuery: UIEventSource = new UIEventSource(false); public readonly retries: UIEventSource = new UIEventSource(0); /** - * The previous bounds for which the query has been run + * The previous bounds for which the query has been run at the given zoom level + * + * Note that some layers only activate on a certain zoom level. + * If the map location changes, we check for each layer if it is loaded: + * we start checking the bounds at the first zoom level the layer might operate. If in bounds - no reload needed, otherwise we continue walking down */ - private previousBounds: Bounds; + private previousBounds: Map = new Map(); /** * The most important layer should go first, as that one gets first pick for the questions @@ -25,6 +29,13 @@ export class LayerUpdater { constructor(state: State) { const self = this; + + let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom)); + this.sufficentlyZoomed = State.state.locationControl.map(location => location.zoom >= minzoom); + for (let i = 0; i < 25; i++) { + // This update removes all data on all layers -> erase the map on lower levels too + this.previousBounds.set(i, []); + } state.locationControl.addCallback(() => { self.update(state) }); @@ -40,13 +51,30 @@ export class LayerUpdater { state = state ?? State.state; for (const layer of state.layoutToUse.data.layers) { if (state.locationControl.data.zoom < layer.minzoom) { - console.log("Not loading layer ", layer.id, " as it needs at least ",layer.minzoom, "zoom") + console.log("Not loading layer ", layer.id, " as it needs at least ", layer.minzoom, "zoom") + continue; + } + + // Check if data for this layer has already been loaded + let previouslyLoaded = false; + for (let z = layer.minzoom; z < 25 && !previouslyLoaded; z++) { + const previousLoadedBounds = this.previousBounds.get(z); + if (previousLoadedBounds == undefined) { + continue; + } + for (const previousLoadedBound of previousLoadedBounds) { + previouslyLoaded = previouslyLoaded || this.IsInBounds(state, previousLoadedBound); + if(previouslyLoaded){ + break; + } + } + } + if (previouslyLoaded) { continue; } filters.push(layer.overpassFilter); } if (filters.length === 0) { - console.log("No layers loaded at all") return undefined; } return new Or(filters); @@ -66,8 +94,8 @@ export class LayerUpdater { } 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); geojson = layer.SetApplicableData(geojson); @@ -94,15 +122,7 @@ export class LayerUpdater { private update(state: State): void { - if (this.IsInBounds(state)) { - return; - } - - const filter = this.GetFilter(state); - - - this.sufficentlyZoomed.setData(filter !== undefined); if (filter === undefined) { return; } @@ -117,16 +137,19 @@ export class LayerUpdater { const diff = state.layoutToUse.data.widenFactor; const n = Math.min(90, bounds.getNorth() + diff); - const e = Math.min( 180,bounds.getEast() + diff); + const e = Math.min(180, bounds.getEast() + diff); const s = Math.max(-90, bounds.getSouth() - diff); const w = Math.max(-180, bounds.getWest() - diff); + const queryBounds = {north: n, east: e, south: s, west: w}; - this.previousBounds = {north: n, east: e, south: s, west: w}; + const z = state.locationControl.data.zoom; + + this.previousBounds.get(z).push(queryBounds); this.runningQuery.setData(true); const self = this; const overpass = new Overpass(filter); - overpass.queryGeoJson(this.previousBounds, + overpass.queryGeoJson(queryBounds, function (data) { self.handleData(data) }, @@ -138,7 +161,7 @@ export class LayerUpdater { } - private IsInBounds(state: State): boolean { + private IsInBounds(state: State, bounds: Bounds): boolean { if (this.previousBounds === undefined) { return false; @@ -146,18 +169,18 @@ export class LayerUpdater { const b = state.bm.map.getBounds(); - if (b.getSouth() < this.previousBounds.south) { + if (b.getSouth() < bounds.south) { return false; } - if (b.getNorth() > this.previousBounds.north) { + if (b.getNorth() > bounds.north) { return false; } - if (b.getEast() > this.previousBounds.east) { + if (b.getEast() > bounds.east) { return false; } - if (b.getWest() < this.previousBounds.west) { + if (b.getWest() < bounds.west) { return false; } diff --git a/Logic/Leaflet/Basemap.ts b/Logic/Leaflet/Basemap.ts index 89ebcd581e..88cf722d3e 100644 --- a/Logic/Leaflet/Basemap.ts +++ b/Logic/Leaflet/Basemap.ts @@ -82,6 +82,12 @@ export class Basemap { }); + // Users are not allowed to zoom to the 'copies' on the left and the right, stuff goes wrong then + // We give a bit of leeway for people on the edges + // Also see: https://www.reddit.com/r/openstreetmap/comments/ih4zzc/mapcomplete_a_new_easytouse_editor/g31ubyv/ + this.map.setMaxBounds( + [[-100,-200],[100,200]] + ); this.map.attributionControl.setPrefix( extraAttribution.Render() + " | OpenStreetMap"); this.Location = location; diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index fa2a5e436a..b6f5f6eefa 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -130,9 +130,7 @@ export class OsmConnection { }, function (err, details) { if(err != null){ console.log(err); - self.auth.logout(); - self.userDetails.data.loggedIn = false; - self.userDetails.ping(); + return; } if (details == null) { diff --git a/Logic/Osm/OsmImageUploadHandler.ts b/Logic/Osm/OsmImageUploadHandler.ts index 925debd305..f7c73da69a 100644 --- a/Logic/Osm/OsmImageUploadHandler.ts +++ b/Logic/Osm/OsmImageUploadHandler.ts @@ -7,6 +7,7 @@ import {ImageUploadFlow} from "../../UI/ImageUploadFlow"; import {UserDetails} from "./OsmConnection"; import {SlideShow} from "../../UI/SlideShow"; import {State} from "../../State"; +import {Tag} from "../TagsFilter"; export class OsmImageUploadHandler { private _tags: UIEventSource; @@ -51,7 +52,7 @@ export class OsmImageUploadHandler { key = "image:" + freeIndex; } console.log("Adding image:" + key, url); - changes.addChange(tags.id, key, url); + changes.addTag(tags.id, new Tag(key, url)); self._slideShow.MoveTo(-1); // set the last (thus newly added) image) to view }, allDone: () => { diff --git a/Logic/PersonalLayersPanel.ts b/Logic/PersonalLayersPanel.ts index 3f16b02f8c..3b9a680a81 100644 --- a/Logic/PersonalLayersPanel.ts +++ b/Logic/PersonalLayersPanel.ts @@ -94,7 +94,7 @@ export class PersonalLayersPanel extends UIElement { ]), controls[layer.id] ?? (favs.indexOf(layer.id) >= 0) ); - cb.clss = "custom-layer-checkbox" + cb.SetClass("custom-layer-checkbox"); controls[layer.id] = cb.isEnabled; cb.isEnabled.addCallback((isEnabled) => { diff --git a/State.ts b/State.ts index b19c6dd62d..d7fa4d8a82 100644 --- a/State.ts +++ b/State.ts @@ -24,7 +24,7 @@ export class State { // The singleton of the global state public static state: State; - public static vNumber = "0.0.7b Less changesets"; + public static vNumber = "0.0.7c mutlizoom"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { @@ -133,9 +133,9 @@ export class State { lat: Utils.asFloat(this.lat.data), lon: Utils.asFloat(this.lon.data), }).addCallback((latlonz) => { - this.zoom.setData(latlonz.zoom.toString()); - this.lat.setData(latlonz.lat.toString().substr(0, 6)); - this.lon.setData(latlonz.lon.toString().substr(0, 6)); + this.zoom.setData(latlonz.zoom?.toString()); + this.lat.setData(latlonz.lat?.toString()?.substr(0, 6)); + this.lon.setData(latlonz.lon?.toString()?.substr(0, 6)); }); this.layoutToUse.addCallback(layoutToUse => { diff --git a/UI/MoreScreen.ts b/UI/MoreScreen.ts index 6548ae1412..ae6a69da1a 100644 --- a/UI/MoreScreen.ts +++ b/UI/MoreScreen.ts @@ -34,7 +34,7 @@ export class MoreScreen extends UIElement { const currentLocation = State.state.locationControl.data; let linkText = - `./${layout.name}.html?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` + `./${layout.name.toLowerCase()}.html?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { linkText = `./index.html?layout=${layout.name}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` @@ -80,16 +80,16 @@ export class MoreScreen extends UIElement { for (const k in AllKnownLayouts.allSets) { - - + const layout : Layout = AllKnownLayouts.allSets[k]; if (k === PersonalLayout.NAME) { if (State.state.osmConnection.userDetails.data.csCount < State.userJourney.customLayoutUnlock) { continue; } } - - - els.push(this.createLinkButton(AllKnownLayouts.allSets[k])); + if(layout.name !== k){ + continue; // This layout was added multiple time due to an uppercase + } + els.push(this.createLinkButton(layout)); } diff --git a/UI/i18n/Translations.ts b/UI/i18n/Translations.ts index a66e716ad5..3be2edf7f0 100644 --- a/UI/i18n/Translations.ts +++ b/UI/i18n/Translations.ts @@ -239,7 +239,12 @@ export default class Translations { fr: 'Est-ce que la pompe à un manomètre integré?', gl: 'Ten a bomba de ar un indicador de presión ou un manómetro?' }), - yes: new T({en: 'There is a manometer', nl: 'Er is een luchtdrukmeter', fr: 'Il y a un manomètre'}), + yes: new T({ + en: 'There is a manometer', + nl: 'Er is een luchtdrukmeter', + fr: 'Il y a un manomètre', + gl: 'Hai manómetro' + }), no: new T({ en: 'There is no manometer', nl: 'Er is geen luchtdrukmeter', diff --git a/assets/themes/cyclestreets/cyclestreets.json b/assets/themes/cyclestreets/cyclestreets.json index 6ab6ef750e..0fa369e3e0 100644 --- a/assets/themes/cyclestreets/cyclestreets.json +++ b/assets/themes/cyclestreets/cyclestreets.json @@ -15,7 +15,7 @@ "render": "#0000ff" }, "description": "Een fietsstraat is een straat waar gemotoriseerd verkeer een fietser niet mag inhalen.", - "minzoom": "16", + "minzoom": 9, "presets": [], "tagRenderings": [], "overpassTags": "cyclestreet=yes", @@ -54,7 +54,7 @@ "render": "5" }, "description": "Deze straat wordt binnenkort een fietsstraat", - "minzoom": "16", + "minzoom": "9", "wayHandling": 0, "presets": [], "tagRenderings": [{ @@ -121,7 +121,7 @@ } ], "type": "text", - "question": "Is deze straat een fietsstraat?", + "question": "Is deze straat een fietsstraat?" }, { "key": "cyclestreet:start_date", @@ -132,7 +132,7 @@ } ], "overpassTags": "highway~=residential|tertiary|unclassified", - "minzoom": "13" + "minzoom": "18" } ], "language": "nl", @@ -143,6 +143,6 @@ "title": "Fietsstraten", "startLon": "3.2228", "icon": "./assets/themes/cyclestreets/F111.svg", - "description": "Een fietsstraat is een straat waar automobilisten geen fietsers mogen inhalen en waar een maximumsnelheid van 30km/h geldt.

Op deze open kaart kan je alle gekende fietsstraten zien en kan je ontbrekende fietsstraten aanduiden.", - "widenFactor": 0.03 + "description": "Een fietsstraat is een straat waar automobilisten geen fietsers mogen inhalen en waar een maximumsnelheid van 30km/u geldt.

Op deze open kaart kan je alle gekende fietsstraten zien en kan je ontbrekende fietsstraten aanduiden. Om de kaart aan te passen, moet je je aanmelden met OpenStreetMap en helemaal inzoomen tot straatniveau.", + "widenfactor": 0.05 } \ No newline at end of file diff --git a/index.ts b/index.ts index 4e7dc993cd..6a33bc7857 100644 --- a/index.ts +++ b/index.ts @@ -47,13 +47,13 @@ let hash = window.location.hash; const path = window.location.pathname.split("/").slice(-1)[0]; if (path !== "index.html") { defaultLayout = path.substr(0, path.length - 5); - console.log("Using", defaultLayout) + console.log("Using layout", defaultLayout) } // Run over all questsets. If a part of the URL matches a searched-for part in the layout, it'll take that as the default for (const k in AllKnownLayouts.allSets) { const layout = AllKnownLayouts.allSets[k]; - const possibleParts = layout.locationContains ?? []; + const possibleParts = (layout.locationContains ?? []); for (const locationMatch of possibleParts) { if (locationMatch === "") { continue @@ -66,7 +66,7 @@ for (const k in AllKnownLayouts.allSets) { defaultLayout = QueryParameters.GetQueryParameter("layout", defaultLayout).data; -let layoutToUse: Layout = AllKnownLayouts.allSets[defaultLayout] ?? AllKnownLayouts["all"]; +let layoutToUse: Layout = AllKnownLayouts.allSets[defaultLayout.toLowerCase()] ?? AllKnownLayouts["all"]; const userLayoutParam = QueryParameters.GetQueryParameter("userlayout", "false");