diff --git a/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts index 8d42847..6c40605 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 584ecd5..6a6e415 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 34db35c..bf876ab 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 7282257..4e5afb6 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 b4a1ffd..61f09ce 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 34c2603..d5e5181 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 6ab99e5..f79b31d 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 e3c3f09..a89ff4e 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 89ebcd5..88cf722 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 fa2a5e4..b6f5f6e 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 925debd..f7c73da 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 3f16b02..3b9a680 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 b19c6dd..d7fa4d8 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 6548ae1..ae6a69d 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 a66e716..3be2edf 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 6ab6ef7..0fa369e 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 4e7dc99..6a33bc7 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");