More work on cyclestreet layout, add loading of layers depending on zoom level

This commit is contained in:
Pieter Vander Vennet 2020-08-28 03:16:21 +02:00
parent 3576a4b1e1
commit 9a5b35b9f3
17 changed files with 109 additions and 59 deletions

View file

@ -57,6 +57,7 @@ export class AllKnownLayouts {
continue; continue;
} }
this.allLayers[layer.id] = layer; this.allLayers[layer.id] = layer;
this.allLayers[layer.id.toLowerCase()] = layer;
all.layers.push(layer); all.layers.push(layer);
} }
} }
@ -64,6 +65,7 @@ export class AllKnownLayouts {
const allSets: Map<string, Layout> = new Map(); const allSets: Map<string, Layout> = new Map();
for (const layout of this.layoutsList) { for (const layout of this.layoutsList) {
allSets[layout.name] = layout; allSets[layout.name] = layout;
allSets[layout.name.toLowerCase()] = layout;
} }
allSets[all.name] = all; allSets[all.name] = all;
return allSets; return allSets;

View file

@ -45,6 +45,7 @@ export interface LayerConfigJson {
width?: TagRenderingConfigJson; width?: TagRenderingConfigJson;
overpassTags: string | { k: string, v: string }[]; overpassTags: string | { k: string, v: string }[];
wayHandling?: number, wayHandling?: number,
widenFactor?: number,
presets: { presets: {
tags: string, tags: string,
title: string | any, title: string | any,

View file

@ -106,6 +106,7 @@ export class LayerDefinition {
elementsToShow?: TagDependantUIElementConstructor[], elementsToShow?: TagDependantUIElementConstructor[],
maxAllowedOverlapPercentage?: number, maxAllowedOverlapPercentage?: number,
wayHandling?: number, wayHandling?: number,
widenFactor?: number,
style?: (tags: any) => { style?: (tags: any) => {
color: string, color: string,
icon: any icon: any

View file

@ -13,7 +13,7 @@ export default class Cyclofix extends Layout {
constructor() { constructor() {
super( super(
"cyclofix", "cyclofix",
["en", "nl", "fr"], ["en", "nl", "fr","gl"],
Translations.t.cyclofix.title, Translations.t.cyclofix.title,
[new BikeServices(), new BikeShops(), new DrinkingWater(), new BikeParkings(), new BikeOtherShops(), new BikeCafes()], [new BikeServices(), new BikeShops(), new DrinkingWater(), new BikeParkings(), new BikeOtherShops(), new BikeCafes()],
16, 16,

View file

@ -183,7 +183,6 @@ export class InitUiElements {
const flayers: FilteredLayer[] = [] const flayers: FilteredLayer[] = []
const presets: Preset[] = []; const presets: Preset[] = [];
let minZoom = 0;
const state = State.state; const state = State.state;
for (const layer of state.layoutToUse.data.layers) { 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 ?? []) { for (const preset of layer.presets ?? []) {
if (preset.icon === undefined) { if (preset.icon === undefined) {

View file

@ -29,7 +29,7 @@ export class FilteredLayer {
/** The featurecollection from overpass /** The featurecollection from overpass
*/ */
private _dataFromOverpass; private _dataFromOverpass : any[];
private _wayHandling: number; private _wayHandling: number;
/** List of new elements, geojson features /** List of new elements, geojson features
*/ */
@ -146,7 +146,7 @@ export class FilteredLayer {
public AddNewElement(element) { public AddNewElement(element) {
this._newElements.push(element); this._newElements.push(element);
console.log("Element added"); 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; let self = this;
if (this._geolayer !== undefined && this._geolayer !== null) { 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); 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<number> = new Set<number>();
// A list of all the features to show
const fusedFeatures = []; const fusedFeatures = [];
const idsFromOverpass = []; // First, we add all the fresh data:
for (const feature of data.features) { 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); fusedFeatures.push(feature);
} }
for (const feature of this._newElements) { 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 // This element is not yet uploaded or not yet visible in overpass
// We include it in the layer // We include it in the layer
fusedFeatures.push(feature); fusedFeatures.push(feature);
} }
} }
this._dataFromOverpass = fusedFeatures;
// We use a new, fused dataset // We use a new, fused dataset
data = { data = {

View file

@ -6,6 +6,7 @@ import {ImgurImage} from "../UI/Image/ImgurImage";
import {State} from "../State"; import {State} from "../State";
import {ImagesInCategory, Wikidata, Wikimedia} from "./Web/Wikimedia"; import {ImagesInCategory, Wikidata, Wikimedia} from "./Web/Wikimedia";
import {UIEventSource} from "./UIEventSource"; import {UIEventSource} from "./UIEventSource";
import {Tag} from "./TagsFilter";
/** /**
* There are multiple way to fetch images for an object * There are multiple way to fetch images for an object
@ -121,7 +122,7 @@ export class ImageSearcher extends UIEventSource<string[]> {
return; return;
} }
console.log("Deleting image...", key, " --> ", url); 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.data.push(url);
this._deletedImages.ping(); this._deletedImages.ping();
} }

View file

@ -8,13 +8,17 @@ import {State} from "../State";
export class LayerUpdater { export class LayerUpdater {
public readonly sufficentlyZoomed: UIEventSource<boolean> = new UIEventSource<boolean>(false); public readonly sufficentlyZoomed: UIEventSource<boolean>;
public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false); public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false);
public readonly retries: UIEventSource<number> = new UIEventSource<number>(0); public readonly retries: UIEventSource<number> = new UIEventSource<number>(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<number, Bounds[]> = new Map<number, Bounds[]>();
/** /**
* The most important layer should go first, as that one gets first pick for the questions * 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) { constructor(state: State) {
const self = this; 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(() => { state.locationControl.addCallback(() => {
self.update(state) self.update(state)
}); });
@ -40,13 +51,30 @@ export class LayerUpdater {
state = state ?? State.state; state = state ?? State.state;
for (const layer of state.layoutToUse.data.layers) { for (const layer of state.layoutToUse.data.layers) {
if (state.locationControl.data.zoom < layer.minzoom) { 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; continue;
} }
filters.push(layer.overpassFilter); filters.push(layer.overpassFilter);
} }
if (filters.length === 0) { if (filters.length === 0) {
console.log("No layers loaded at all")
return undefined; return undefined;
} }
return new Or(filters); return new Or(filters);
@ -66,8 +94,8 @@ export class LayerUpdater {
} }
return; return;
} }
// We use window.setTimeout to give JS some time to update everything and make the interface not too laggy
window.setTimeout(() => { window.setTimeout(() => {
const layer = layers[0]; const layer = layers[0];
const rest = layers.slice(1, layers.length); const rest = layers.slice(1, layers.length);
geojson = layer.SetApplicableData(geojson); geojson = layer.SetApplicableData(geojson);
@ -94,15 +122,7 @@ export class LayerUpdater {
private update(state: State): void { private update(state: State): void {
if (this.IsInBounds(state)) {
return;
}
const filter = this.GetFilter(state); const filter = this.GetFilter(state);
this.sufficentlyZoomed.setData(filter !== undefined);
if (filter === undefined) { if (filter === undefined) {
return; return;
} }
@ -117,16 +137,19 @@ export class LayerUpdater {
const diff = state.layoutToUse.data.widenFactor; const diff = state.layoutToUse.data.widenFactor;
const n = Math.min(90, bounds.getNorth() + diff); 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 s = Math.max(-90, bounds.getSouth() - diff);
const w = Math.max(-180, bounds.getWest() - 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); this.runningQuery.setData(true);
const self = this; const self = this;
const overpass = new Overpass(filter); const overpass = new Overpass(filter);
overpass.queryGeoJson(this.previousBounds, overpass.queryGeoJson(queryBounds,
function (data) { function (data) {
self.handleData(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) { if (this.previousBounds === undefined) {
return false; return false;
@ -146,18 +169,18 @@ export class LayerUpdater {
const b = state.bm.map.getBounds(); const b = state.bm.map.getBounds();
if (b.getSouth() < this.previousBounds.south) { if (b.getSouth() < bounds.south) {
return false; return false;
} }
if (b.getNorth() > this.previousBounds.north) { if (b.getNorth() > bounds.north) {
return false; return false;
} }
if (b.getEast() > this.previousBounds.east) { if (b.getEast() > bounds.east) {
return false; return false;
} }
if (b.getWest() < this.previousBounds.west) { if (b.getWest() < bounds.west) {
return false; return false;
} }

View file

@ -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( this.map.attributionControl.setPrefix(
extraAttribution.Render() + " | <a href='https://osm.org'>OpenStreetMap</a>"); extraAttribution.Render() + " | <a href='https://osm.org'>OpenStreetMap</a>");
this.Location = location; this.Location = location;

View file

@ -130,9 +130,7 @@ export class OsmConnection {
}, function (err, details) { }, function (err, details) {
if(err != null){ if(err != null){
console.log(err); console.log(err);
self.auth.logout(); return;
self.userDetails.data.loggedIn = false;
self.userDetails.ping();
} }
if (details == null) { if (details == null) {

View file

@ -7,6 +7,7 @@ import {ImageUploadFlow} from "../../UI/ImageUploadFlow";
import {UserDetails} from "./OsmConnection"; import {UserDetails} from "./OsmConnection";
import {SlideShow} from "../../UI/SlideShow"; import {SlideShow} from "../../UI/SlideShow";
import {State} from "../../State"; import {State} from "../../State";
import {Tag} from "../TagsFilter";
export class OsmImageUploadHandler { export class OsmImageUploadHandler {
private _tags: UIEventSource<any>; private _tags: UIEventSource<any>;
@ -51,7 +52,7 @@ export class OsmImageUploadHandler {
key = "image:" + freeIndex; key = "image:" + freeIndex;
} }
console.log("Adding image:" + key, url); 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 self._slideShow.MoveTo(-1); // set the last (thus newly added) image) to view
}, },
allDone: () => { allDone: () => {

View file

@ -94,7 +94,7 @@ export class PersonalLayersPanel extends UIElement {
]), ]),
controls[layer.id] ?? (favs.indexOf(layer.id) >= 0) controls[layer.id] ?? (favs.indexOf(layer.id) >= 0)
); );
cb.clss = "custom-layer-checkbox" cb.SetClass("custom-layer-checkbox");
controls[layer.id] = cb.isEnabled; controls[layer.id] = cb.isEnabled;
cb.isEnabled.addCallback((isEnabled) => { cb.isEnabled.addCallback((isEnabled) => {

View file

@ -24,7 +24,7 @@ export class State {
// The singleton of the global state // The singleton of the global state
public static state: 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 // The user journey states thresholds when a new feature gets unlocked
public static userJourney = { public static userJourney = {
@ -133,9 +133,9 @@ export class State {
lat: Utils.asFloat(this.lat.data), lat: Utils.asFloat(this.lat.data),
lon: Utils.asFloat(this.lon.data), lon: Utils.asFloat(this.lon.data),
}).addCallback((latlonz) => { }).addCallback((latlonz) => {
this.zoom.setData(latlonz.zoom.toString()); this.zoom.setData(latlonz.zoom?.toString());
this.lat.setData(latlonz.lat.toString().substr(0, 6)); this.lat.setData(latlonz.lat?.toString()?.substr(0, 6));
this.lon.setData(latlonz.lon.toString().substr(0, 6)); this.lon.setData(latlonz.lon?.toString()?.substr(0, 6));
}); });
this.layoutToUse.addCallback(layoutToUse => { this.layoutToUse.addCallback(layoutToUse => {

View file

@ -34,7 +34,7 @@ export class MoreScreen extends UIElement {
const currentLocation = State.state.locationControl.data; const currentLocation = State.state.locationControl.data;
let linkText = 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") { 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}` 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) { for (const k in AllKnownLayouts.allSets) {
const layout : Layout = AllKnownLayouts.allSets[k];
if (k === PersonalLayout.NAME) { if (k === PersonalLayout.NAME) {
if (State.state.osmConnection.userDetails.data.csCount < State.userJourney.customLayoutUnlock) { if (State.state.osmConnection.userDetails.data.csCount < State.userJourney.customLayoutUnlock) {
continue; continue;
} }
} }
if(layout.name !== k){
continue; // This layout was added multiple time due to an uppercase
els.push(this.createLinkButton(AllKnownLayouts.allSets[k])); }
els.push(this.createLinkButton(layout));
} }

View file

@ -239,7 +239,12 @@ export default class Translations {
fr: 'Est-ce que la pompe à un manomètre integré?', 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?' 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({ no: new T({
en: 'There is no manometer', en: 'There is no manometer',
nl: 'Er is geen luchtdrukmeter', nl: 'Er is geen luchtdrukmeter',

View file

@ -15,7 +15,7 @@
"render": "#0000ff" "render": "#0000ff"
}, },
"description": "Een fietsstraat is een straat waar gemotoriseerd verkeer een fietser niet mag inhalen.", "description": "Een fietsstraat is een straat waar gemotoriseerd verkeer een fietser niet mag inhalen.",
"minzoom": "16", "minzoom": 9,
"presets": [], "presets": [],
"tagRenderings": [], "tagRenderings": [],
"overpassTags": "cyclestreet=yes", "overpassTags": "cyclestreet=yes",
@ -54,7 +54,7 @@
"render": "5" "render": "5"
}, },
"description": "Deze straat wordt binnenkort een fietsstraat", "description": "Deze straat wordt binnenkort een fietsstraat",
"minzoom": "16", "minzoom": "9",
"wayHandling": 0, "wayHandling": 0,
"presets": [], "presets": [],
"tagRenderings": [{ "tagRenderings": [{
@ -121,7 +121,7 @@
} }
], ],
"type": "text", "type": "text",
"question": "Is deze straat een fietsstraat?", "question": "Is deze straat een fietsstraat?"
}, },
{ {
"key": "cyclestreet:start_date", "key": "cyclestreet:start_date",
@ -132,7 +132,7 @@
} }
], ],
"overpassTags": "highway~=residential|tertiary|unclassified", "overpassTags": "highway~=residential|tertiary|unclassified",
"minzoom": "13" "minzoom": "18"
} }
], ],
"language": "nl", "language": "nl",
@ -143,6 +143,6 @@
"title": "Fietsstraten", "title": "Fietsstraten",
"startLon": "3.2228", "startLon": "3.2228",
"icon": "./assets/themes/cyclestreets/F111.svg", "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.<br/><br/>Op deze open kaart kan je alle gekende fietsstraten zien en kan je ontbrekende fietsstraten aanduiden.", "description": "Een fietsstraat is een straat waar <b>automobilisten geen fietsers mogen inhalen</b> en waar een maximumsnelheid van <b>30km/u</b> geldt.<br/><br/>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.03 "widenfactor": 0.05
} }

View file

@ -47,13 +47,13 @@ let hash = window.location.hash;
const path = window.location.pathname.split("/").slice(-1)[0]; const path = window.location.pathname.split("/").slice(-1)[0];
if (path !== "index.html") { if (path !== "index.html") {
defaultLayout = path.substr(0, path.length - 5); 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 // 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) { for (const k in AllKnownLayouts.allSets) {
const layout = AllKnownLayouts.allSets[k]; const layout = AllKnownLayouts.allSets[k];
const possibleParts = layout.locationContains ?? []; const possibleParts = (layout.locationContains ?? []);
for (const locationMatch of possibleParts) { for (const locationMatch of possibleParts) {
if (locationMatch === "") { if (locationMatch === "") {
continue continue
@ -66,7 +66,7 @@ for (const k in AllKnownLayouts.allSets) {
defaultLayout = QueryParameters.GetQueryParameter("layout", defaultLayout).data; 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"); const userLayoutParam = QueryParameters.GetQueryParameter("userlayout", "false");