From 09897b47e04e50ab748b88d46df38597d1108cba Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 29 Sep 2021 16:55:05 +0200 Subject: [PATCH] Add mssing assets --- Logic/Actors/OverpassFeatureSource.ts | 108 ++++++++---------- Logic/FeatureSource/FeaturePipeline.ts | 22 ++-- Logic/Osm/Overpass.ts | 12 +- Models/Bounds.ts | 6 - Models/Constants.ts | 10 ++ Models/ThemeConfig/Json/LayoutConfigJson.ts | 4 +- Models/ThemeConfig/LayoutConfig.ts | 12 +- State.ts | 6 +- Utils.ts | 1 + .../toerisme_vlaanderen/license_info.json | 12 ++ assets/themes/toerisme_vlaanderen/logo.png | Bin 0 -> 11674 bytes 11 files changed, 104 insertions(+), 89 deletions(-) delete mode 100644 Models/Bounds.ts create mode 100644 assets/themes/toerisme_vlaanderen/license_info.json create mode 100644 assets/themes/toerisme_vlaanderen/logo.png diff --git a/Logic/Actors/OverpassFeatureSource.ts b/Logic/Actors/OverpassFeatureSource.ts index 2487039542..fa5de3eed3 100644 --- a/Logic/Actors/OverpassFeatureSource.ts +++ b/Logic/Actors/OverpassFeatureSource.ts @@ -2,7 +2,6 @@ import {UIEventSource} from "../UIEventSource"; import Loc from "../../Models/Loc"; import {Or} from "../Tags/Or"; import {Overpass} from "../Osm/Overpass"; -import Bounds from "../../Models/Bounds"; import FeatureSource from "../FeatureSource/FeatureSource"; import {Utils} from "../../Utils"; import {TagsFilter} from "../Tags/TagsFilter"; @@ -36,16 +35,17 @@ export default class OverpassFeatureSource implements FeatureSource { * 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 readonly _previousBounds: Map = new Map(); + private readonly _previousBounds: Map = new Map(); private readonly state: { readonly locationControl: UIEventSource, readonly layoutToUse: LayoutConfig, - readonly overpassUrl: UIEventSource; + readonly overpassUrl: UIEventSource; readonly overpassTimeout: UIEventSource; - readonly currentBounds :UIEventSource + readonly currentBounds: UIEventSource } private readonly _isActive: UIEventSource; private _onUpdated?: (bbox: BBox, dataFreshness: Date) => void; + /** * The most important layer should go first, as that one gets first pick for the questions */ @@ -53,20 +53,20 @@ export default class OverpassFeatureSource implements FeatureSource { state: { readonly locationControl: UIEventSource, readonly layoutToUse: LayoutConfig, - readonly overpassUrl: UIEventSource; + readonly overpassUrl: UIEventSource; readonly overpassTimeout: UIEventSource; readonly overpassMaxZoom: UIEventSource, - readonly currentBounds :UIEventSource - }, - - options?: { + readonly currentBounds: UIEventSource + }, + options?: { isActive?: UIEventSource, - onUpdated?: (bbox: BBox, freshness: Date) => void, - relationTracker: RelationsTracker}) { + onUpdated?: (bbox: BBox, freshness: Date) => void, + relationTracker: RelationsTracker + }) { this.state = state this._isActive = options.isActive; - this._onUpdated =options. onUpdated; + this._onUpdated = options.onUpdated; this.relationsTracker = options.relationTracker const location = state.locationControl const self = this; @@ -79,14 +79,14 @@ export default class OverpassFeatureSource implements FeatureSource { location.addCallback(() => { self.update() }); - + state.currentBounds.addCallback(_ => { self.update() }) - + } - private GetFilter(): Overpass { + private GetFilter(interpreterUrl: string): Overpass { let filters: TagsFilter[] = []; let extraScripts: string[] = []; for (const layer of this.state.layoutToUse.layers) { @@ -113,7 +113,7 @@ export default class OverpassFeatureSource implements FeatureSource { continue; } for (const previousLoadedBound of previousLoadedBounds) { - previouslyLoaded = previouslyLoaded || this.IsInBounds(previousLoadedBound); + previouslyLoaded = previouslyLoaded || this.state.currentBounds.data.isContainedIn(previousLoadedBound); if (previouslyLoaded) { break; } @@ -133,16 +133,16 @@ export default class OverpassFeatureSource implements FeatureSource { if (filters.length + extraScripts.length === 0) { return undefined; } - return new Overpass(new Or(filters), extraScripts, this.state.overpassUrl, this.state.overpassTimeout, this.relationsTracker); + return new Overpass(new Or(filters), extraScripts, interpreterUrl, this.state.overpassTimeout, this.relationsTracker); } private update() { - if(!this._isActive.data){ + if (!this._isActive.data) { return; } const self = this this.updateAsync().then(bboxAndDate => { - if(bboxAndDate === undefined || self._onUpdated === undefined){ + if (bboxAndDate === undefined || self._onUpdated === undefined) { return; } const [bbox, date] = bboxAndDate @@ -162,51 +162,54 @@ export default class OverpassFeatureSource implements FeatureSource { } const bounds = this.state.currentBounds.data?.pad(this.state.layoutToUse.widenFactor)?.expandToTileBounds(14); - + if (bounds === undefined) { return undefined; } - - const n = Math.min(90, bounds.getNorth()); - const e = Math.min(180, bounds.getEast()); - const s = Math.max(-90, bounds.getSouth()); - const w = Math.max(-180, bounds.getWest()); - const queryBounds = {north: n, east: e, south: s, west: w}; - - const self = this; - const overpass = this.GetFilter(); - - if (overpass === undefined) { - return undefined; - } - this.runningQuery.setData(true); let data: any = undefined let date: Date = undefined + const overpassUrls = self.state.overpassUrl.data + let lastUsed = 0; do { - try { - [data, date] = await overpass.queryGeoJson(queryBounds) + const overpass = this.GetFilter(overpassUrls[lastUsed]); + + if (overpass === undefined) { + return undefined; + } + this.runningQuery.setData(true); + + [data, date] = await overpass.queryGeoJson(bounds) console.log("Querying overpass is done", data) } catch (e) { self.retries.data++; self.retries.ping(); - console.error(`QUERY FAILED (retrying in ${5 * self.retries.data} sec) due to`, e); + console.error(`QUERY FAILED due to`, e); - self.timeout.setData(self.retries.data * 5); - - while (self.timeout.data > 0) { - await Utils.waitFor(1000) - self.timeout.data-- - self.timeout.ping(); + await Utils.waitFor(1000) + + if (lastUsed + 1 < overpassUrls.length) { + lastUsed++ + console.log("Trying next time with", overpassUrls[lastUsed]) + } else { + lastUsed = 0 + self.timeout.setData(self.retries.data * 5); + + while (self.timeout.data > 0) { + await Utils.waitFor(1000) + console.log(self.timeout.data) + self.timeout.data-- + self.timeout.ping(); + } } } } while (data === undefined); const z = Math.floor(this.state.locationControl.data.zoom ?? 0); - self._previousBounds.get(z).push(queryBounds); + self._previousBounds.get(z).push(bounds); self.retries.setData(0); try { @@ -215,25 +218,10 @@ export default class OverpassFeatureSource implements FeatureSource { return [bounds, date]; } catch (e) { console.error("Got the overpass response, but could not process it: ", e, e.stack) - }finally { + } finally { self.runningQuery.setData(false); } - } - - private IsInBounds(bounds: Bounds): boolean { - if (this._previousBounds === undefined) { - return false; - } - - const b = this.state.currentBounds.data; - return b.getSouth() >= bounds.south && - b.getNorth() <= bounds.north && - b.getEast() <= bounds.east && - b.getWest() >= bounds.west; - } - - } \ No newline at end of file diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index e8d270f0b6..3db3a039d4 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -30,10 +30,10 @@ import {Tiles} from "../../Models/TileRange"; export default class FeaturePipeline { public readonly sufficientlyZoomed: UIEventSource; - + public readonly runningQuery: UIEventSource; public readonly timeout: UIEventSource; - + public readonly somethingLoaded: UIEventSource = new UIEventSource(false) public readonly newDataLoadedSignal: UIEventSource = new UIEventSource(undefined) @@ -50,7 +50,7 @@ export default class FeaturePipeline { readonly changes: Changes, readonly layoutToUse: LayoutConfig, readonly leafletMap: any, - readonly overpassUrl: UIEventSource; + readonly overpassUrl: UIEventSource; readonly overpassTimeout: UIEventSource; readonly overpassMaxZoom: UIEventSource; readonly osmConnection: OsmConnection @@ -67,6 +67,7 @@ export default class FeaturePipeline { const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12)) this.relationTracker = new RelationsTracker() + console.log("Tilefreshnesses are", tileFreshnesses.data) const oldestAllowedDate = new Date(new Date().getTime() - (60 * 60 * 24 * 30 * 1000)); const neededTilesFromOsm = state.currentBounds.map(bbox => { @@ -90,7 +91,7 @@ export default class FeaturePipeline { }) return tileIndexes }, [tileFreshnesses]) - + const updater = new OverpassFeatureSource(state, { relationTracker: this.relationTracker, @@ -105,8 +106,10 @@ export default class FeaturePipeline { } }); - + this.overpassUpdater = updater; + this.timeout = updater.timeout + this.sufficientlyZoomed = state.locationControl.map(location => { if (location?.zoom === undefined) { return false; @@ -115,10 +118,10 @@ export default class FeaturePipeline { return location.zoom >= minzoom; } ); - + this.timeout = updater.timeout - - + + // Register everything in the state' 'AllElements' new RegisteringAllFromFeatureSourceActor(updater) @@ -203,9 +206,8 @@ export default class FeaturePipeline { } } - - const osmFeatureSource = new OsmFeatureSource({ + const osmFeatureSource = new OsmFeatureSource({ isActive: useOsmApi, neededTiles: neededTilesFromOsm, handleTile: tile => { diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts index 2bf1b4e705..47b92e278a 100644 --- a/Logic/Osm/Overpass.ts +++ b/Logic/Osm/Overpass.ts @@ -1,9 +1,9 @@ import * as OsmToGeoJson from "osmtogeojson"; -import Bounds from "../../Models/Bounds"; import {TagsFilter} from "../Tags/TagsFilter"; import RelationsTracker from "./RelationsTracker"; import {Utils} from "../../Utils"; import {UIEventSource} from "../UIEventSource"; +import {BBox} from "../BBox"; /** * Interfaces overpass to get all the latest data @@ -11,7 +11,7 @@ import {UIEventSource} from "../UIEventSource"; export class Overpass { public static testUrl: string = null private _filter: TagsFilter - private readonly _interpreterUrl: UIEventSource; + private readonly _interpreterUrl: string; private readonly _timeout: UIEventSource; private readonly _extraScripts: string[]; private _includeMeta: boolean; @@ -19,7 +19,7 @@ export class Overpass { constructor(filter: TagsFilter, extraScripts: string[], - interpreterUrl: UIEventSource, + interpreterUrl: string, timeout: UIEventSource, relationTracker: RelationsTracker, includeMeta = true) { @@ -31,9 +31,9 @@ export class Overpass { this._relationTracker = relationTracker } - public async queryGeoJson(bounds: Bounds): Promise<[any, Date]> { + public async queryGeoJson(bounds: BBox): Promise<[any, Date]> { - let query = this.buildQuery("[bbox:" + bounds.south + "," + bounds.west + "," + bounds.north + "," + bounds.east + "]") + let query = this.buildQuery("[bbox:" + bounds.getSouth() + "," + bounds.getWest() + "," + bounds.getNorth() + "," + bounds.getEast() + "]") if (Overpass.testUrl !== null) { console.log("Using testing URL") @@ -68,6 +68,6 @@ export class Overpass { } const query = `[out:json][timeout:${this._timeout.data}]${bbox};(${filter});out body;${this._includeMeta ? 'out meta;' : ''}>;out skel qt;` - return `${this._interpreterUrl.data}?data=${encodeURIComponent(query)}` + return `${this._interpreterUrl}?data=${encodeURIComponent(query)}` } } diff --git a/Models/Bounds.ts b/Models/Bounds.ts deleted file mode 100644 index 3a993c8e06..0000000000 --- a/Models/Bounds.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default interface Bounds { - north: number, - east: number, - south: number, - west: number -} \ No newline at end of file diff --git a/Models/Constants.ts b/Models/Constants.ts index a7f79bf39f..b9855a2930 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -4,6 +4,16 @@ export default class Constants { public static vNumber = "0.10.0-alpha-4"; public static ImgurApiKey = '7070e7167f0a25a' + public static defaultOverpassUrls = [ + // The official instance, 10000 queries per day per project allowed + "https://overpass-api.de/api/interpreter", + // 'Fair usage' + "https://overpass.kumi.systems/api/interpreter", + // "https://overpass.nchc.org.tw/api/interpreter", + "https://overpass.openstreetmap.ru/cgi/interpreter", + // The french api, only 1000 per day per project allowed, so we put it as last resort + "https://overpass.openstreetmap.fr/api/interpreter" + ] // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/Models/ThemeConfig/Json/LayoutConfigJson.ts b/Models/ThemeConfig/Json/LayoutConfigJson.ts index 887649fc4d..73caee7929 100644 --- a/Models/ThemeConfig/Json/LayoutConfigJson.ts +++ b/Models/ThemeConfig/Json/LayoutConfigJson.ts @@ -263,9 +263,9 @@ export interface LayoutConfigJson { enablePdfDownload?: boolean; /** - * Set a different overpass URL. Default: https://overpass-api.de/api/interpreter + * Set one or more overpass URLs to use for this theme.. */ - overpassUrl?: string; + overpassUrl?: string | string[]; /** * Set a different timeout for overpass queries - in seconds. Default: 30s */ diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index 9e63250390..4f85516325 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -6,6 +6,7 @@ import AllKnownLayers from "../../Customizations/AllKnownLayers"; import {Utils} from "../../Utils"; import LayerConfig from "./LayerConfig"; import {LayerConfigJson} from "./Json/LayerConfigJson"; +import Constants from "../Constants"; export default class LayoutConfig { public readonly id: string; @@ -50,7 +51,7 @@ export default class LayoutConfig { How long is the cache valid, in seconds? */ public readonly cacheTimeout?: number; - public readonly overpassUrl: string; + public readonly overpassUrl: string[]; public readonly overpassTimeout: number; public readonly official: boolean; @@ -157,7 +158,14 @@ export default class LayoutConfig { this.enablePdfDownload = json.enablePdfDownload ?? false; this.customCss = json.customCss; this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) - this.overpassUrl = json.overpassUrl ?? "https://overpass-api.de/api/interpreter" + this.overpassUrl = Constants.defaultOverpassUrls + if(json.overpassUrl !== undefined){ + if(typeof json.overpassUrl === "string"){ + this.overpassUrl = [json.overpassUrl] + }else{ + this.overpassUrl = json.overpassUrl + } + } this.overpassTimeout = json.overpassTimeout ?? 30 } diff --git a/State.ts b/State.ts index 0af8e5a648..00dac3bb50 100644 --- a/State.ts +++ b/State.ts @@ -81,7 +81,7 @@ export default class State { public readonly featureSwitchEnableExport: UIEventSource; public readonly featureSwitchFakeUser: UIEventSource; public readonly featureSwitchExportAsPdf: UIEventSource; - public readonly overpassUrl: UIEventSource; + public readonly overpassUrl: UIEventSource; public readonly overpassTimeout: UIEventSource; @@ -321,9 +321,9 @@ export default class State { ); this.overpassUrl = QueryParameters.GetQueryParameter("overpassUrl", - layoutToUse?.overpassUrl, + layoutToUse?.overpassUrl.join(","), "Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter" - ) + ).map(param => param.split(","), [], urls => urls.join(",")) this.overpassTimeout = QueryParameters.GetQueryParameter("overpassTimeout", "" + layoutToUse?.overpassTimeout, diff --git a/Utils.ts b/Utils.ts index 424784205f..bd5cd62a5a 100644 --- a/Utils.ts +++ b/Utils.ts @@ -320,6 +320,7 @@ export class Utils { } xhr.send(); + xhr.onerror = reject } ) } diff --git a/assets/themes/toerisme_vlaanderen/license_info.json b/assets/themes/toerisme_vlaanderen/license_info.json new file mode 100644 index 0000000000..374abea1e6 --- /dev/null +++ b/assets/themes/toerisme_vlaanderen/license_info.json @@ -0,0 +1,12 @@ +[ + { + "path": "logo.png", + "license": "Logo (all rights reserved)", + "authors": [ + "Toerisme Vlaanderen" + ], + "sources": [ + "https://www.toerismevlaanderen.be/" + ] + } +] \ No newline at end of file diff --git a/assets/themes/toerisme_vlaanderen/logo.png b/assets/themes/toerisme_vlaanderen/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6d0d16dd8458cd28b8130b2250618188a2eb8601 GIT binary patch literal 11674 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