diff --git a/Logic/BBox.ts b/Logic/BBox.ts index 0205b15337..78634897b6 100644 --- a/Logic/BBox.ts +++ b/Logic/BBox.ts @@ -1,5 +1,6 @@ import * as turf from "@turf/turf"; import {TileRange, Tiles} from "../Models/TileRange"; +import {GeoOperations} from "./GeoOperations"; export class BBox { @@ -22,7 +23,7 @@ export class BBox { this.minLon = Math.min(this.minLon, coordinate[0]); this.minLat = Math.min(this.minLat, coordinate[1]); } - + this.maxLon = Math.min(this.maxLon, 180) this.maxLat = Math.min(this.maxLat, 90) this.minLon = Math.max(this.minLon, -180) @@ -117,12 +118,12 @@ export class BBox { } pad(factor: number, maxIncrease = 2): BBox { - + const latDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLat - this.minLat) * factor) - const lonDiff =Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor) + const lonDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor) return new BBox([[ this.minLon - lonDiff, - this.minLat - latDiff + this.minLat - latDiff ], [this.maxLon + lonDiff, this.maxLat + latDiff]]) } @@ -161,4 +162,16 @@ export class BBox { const boundslr = Tiles.tile_bounds_lon_lat(lr.z, lr.x, lr.y) return new BBox([].concat(boundsul, boundslr)) } + + toMercator(): { minLat: number, maxLat: number, minLon: number, maxLon: number } { + const [minLon, minLat] = GeoOperations.ConvertWgs84To900913([this.minLon, this.minLat]) + const [maxLon, maxLat] = GeoOperations.ConvertWgs84To900913([this.maxLon, this.maxLat]) + + return { + minLon, maxLon, + minLat, maxLat + } + + + } } \ No newline at end of file diff --git a/Logic/DetermineLayout.ts b/Logic/DetermineLayout.ts index 9af285952e..476131732d 100644 --- a/Logic/DetermineLayout.ts +++ b/Logic/DetermineLayout.ts @@ -18,7 +18,6 @@ export default class DetermineLayout { */ public static async GetLayout(): Promise<[LayoutConfig, string]> { - const loadCustomThemeParam = QueryParameters.GetQueryParameter("userlayout", "false", "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme") const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data); @@ -73,17 +72,13 @@ export default class DetermineLayout { try { - const data = await Utils.downloadJson(link) + const parsed = await Utils.downloadJson(link) + console.log("Got ", parsed) try { - let parsed = data; - if (typeof parsed == "string") { - parsed = JSON.parse(parsed); - } - // Overwrite the id to the url parsed.id = link; - return new LayoutConfig(parsed, false).patchImages(link, data); + return new LayoutConfig(parsed, false).patchImages(link, JSON.stringify(parsed)); } catch (e) { - + console.error(e) DetermineLayout.ShowErrorOnCustomTheme( `${link} is invalid:`, new FixedUiElement(e) @@ -92,6 +87,7 @@ export default class DetermineLayout { } } catch (e) { + console.erorr(e) DetermineLayout.ShowErrorOnCustomTheme( `${link} is invalid - probably not found or invalid JSON:`, new FixedUiElement(e) @@ -107,7 +103,7 @@ export default class DetermineLayout { try { // layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter const dedicatedHashFromLocalStorage = LocalStorageSource.Get( - "user-layout-" + userLayoutParam.data.replace(" ", "_") + "user-layout-" + userLayoutParam.data?.replace(" ", "_") ); if (dedicatedHashFromLocalStorage.data?.length < 10) { dedicatedHashFromLocalStorage.setData(undefined); @@ -134,6 +130,7 @@ export default class DetermineLayout { try { json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash))) } catch (e) { + console.error(e) DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON")) return null; } @@ -143,6 +140,7 @@ export default class DetermineLayout { userLayoutParam.setData(layoutToUse.id); return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))]; } catch (e) { + console.error(e) if (hash === undefined || hash.length < 10) { DetermineLayout.ShowErrorOnCustomTheme("Could not load a theme from the hash", new FixedUiElement("Hash does not contain data")) } diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index b5ea30bd3a..8cd9aae67c 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -98,7 +98,7 @@ export default class FeaturePipeline { this.osmSourceZoomLevel = state.osmApiTileSize.data; const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12)) this.relationTracker = new RelationsTracker() - + state.changes.allChanges.addCallbackAndRun(allChanges => { allChanges.filter(ch => ch.id < 0 && ch.changes !== undefined) .map(ch => ch.changes) @@ -205,7 +205,9 @@ export default class FeaturePipeline { neededTiles: neededTilesFromOsm, handleTile: tile => { new RegisteringAllFromFeatureSourceActor(tile) - new SaveTileToLocalStorageActor(tile, tile.tileIndex) + if (tile.layer.layerDef.maxAgeOfCache > 0) { + new SaveTileToLocalStorageActor(tile, tile.tileIndex) + } perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) @@ -213,7 +215,9 @@ export default class FeaturePipeline { state: state, markTileVisited: (tileId) => state.filteredLayers.data.forEach(flayer => { - SaveTileToLocalStorageActor.MarkVisited(flayer.layerDef.id, tileId, new Date()) + if (flayer.layerDef.maxAgeOfCache > 0) { + SaveTileToLocalStorageActor.MarkVisited(flayer.layerDef.id, tileId, new Date()) + } self.freshnesses.get(flayer.layerDef.id).addTileLoad(tileId, new Date()) }) }) diff --git a/Logic/FeatureSource/Sources/GeoJsonSource.ts b/Logic/FeatureSource/Sources/GeoJsonSource.ts index be93dedc7a..68f8fab823 100644 --- a/Logic/FeatureSource/Sources/GeoJsonSource.ts +++ b/Logic/FeatureSource/Sources/GeoJsonSource.ts @@ -7,6 +7,7 @@ import {Utils} from "../../../Utils"; import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; import {Tiles} from "../../../Models/TileRange"; import {BBox} from "../../BBox"; +import {GeoOperations} from "../../GeoOperations"; export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { @@ -14,7 +15,6 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; public readonly name; public readonly isOsmCache: boolean - private onFail: ((errorMsg: any, url: string) => void) = undefined; private readonly seenids: Set = new Set() public readonly layer: FilteredLayer; @@ -44,10 +44,20 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); if (zxy !== undefined) { const [z, x, y] = zxy; + let tile_bbox = BBox.fromTile(z, x, y) + let bounds : { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox + if(this.layer.layerDef.source.mercatorCrs){ + bounds = tile_bbox.toMercator() + } url = url .replace('{z}', "" + z) .replace('{x}', "" + x) .replace('{y}', "" + y) + .replace('{y_min}',""+bounds.minLat) + .replace('{y_max}',""+bounds.maxLat) + .replace('{x_min}',""+bounds.minLon) + .replace('{x_max}',""+bounds.maxLon) + this.tileIndex = Tiles.tile_index(z, x, y) this.bbox = BBox.fromTile(z, x, y) } else { @@ -71,6 +81,10 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { if(json.features === undefined || json.features === null){ return; } + + if(self.layer.layerDef.source.mercatorCrs){ + json = GeoOperations.GeoJsonToWGS84(json) + } const time = new Date(); const newFeatures: { feature: any, freshness: Date } [] = [] diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts index 5032f53eaa..21aeec1c1d 100644 --- a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts @@ -20,24 +20,28 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { if (source.geojsonSource === undefined) { throw "Invalid layer: geojsonSource expected" } - - const whitelistUrl = source.geojsonSource - .replace("{z}", ""+source.geojsonZoomLevel) - .replace("{x}_{y}.geojson", "overview.json") - .replace("{layer}",layer.layerDef.id) - + let whitelist = undefined - Utils.downloadJson(whitelistUrl).then( - json => { - const data = new Map>(); - for (const x in json) { - data.set(Number(x), new Set(json[x])) + if (source.geojsonSource.indexOf("{x}_{y}.geojson") > 0) { + + const whitelistUrl = source.geojsonSource + .replace("{z}", "" + source.geojsonZoomLevel) + .replace("{x}_{y}.geojson", "overview.json") + .replace("{layer}", layer.layerDef.id) + + Utils.downloadJson(whitelistUrl).then( + json => { + const data = new Map>(); + for (const x in json) { + data.set(Number(x), new Set(json[x])) + } + console.log("The whitelist is", data, "based on ", json, "from", whitelistUrl) + whitelist = data } - whitelist = data - } - ).catch(err => { - console.warn("No whitelist found for ", layer.layerDef.id, err) - }) + ).catch(err => { + console.warn("No whitelist found for ", layer.layerDef.id, err) + }) + } const seenIds = new Set(); const blackList = new UIEventSource(seenIds) @@ -45,14 +49,14 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { layer, source.geojsonZoomLevel, (zxy) => { - if(whitelist !== undefined){ + if (whitelist !== undefined) { const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2]) - if(!isWhiteListed){ + if (!isWhiteListed) { console.log("Not downloading tile", ...zxy, "as it is not on the whitelist") return undefined; } } - + const src = new GeoJsonSource( layer, zxy, diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 87716003ee..09a2881178 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -226,7 +226,7 @@ export class GeoOperations { /** * Generates the closest point on a way from a given point - * + * * The properties object will contain three values: // - `index`: closest point was found on nth line part, // - `dist`: distance between pt and the closest point (in kilometer), @@ -283,6 +283,34 @@ export class GeoOperations { return headerValuesOrdered.map(v => JSON.stringify(v)).join(",") + "\n" + lines.join("\n") } + + private static readonly _earthRadius = 6378137; + private static readonly _originShift = 2 * Math.PI * GeoOperations._earthRadius / 2; + + //Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913 + public static ConvertWgs84To900913(lonLat: [number, number]): [number, number] { + const lon = lonLat[0]; + const lat = lonLat[1]; + const x = lon * GeoOperations._originShift / 180; + let y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); + y = y * GeoOperations._originShift / 180; + return [x, y]; + } + +//Converts XY point from (Spherical) Web Mercator EPSG:3785 (unofficially EPSG:900913) to lat/lon in WGS84 Datum + public static Convert900913ToWgs84(lonLat: [number, number]): [number, number] { + const lon = lonLat[0] + const lat = lonLat[1] + const x = 180 * lon / GeoOperations._originShift; + let y = 180 * lat / GeoOperations._originShift; + y = 180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2); + return [x, y]; + } + + public static GeoJsonToWGS84(geojson){ + return turf.toWgs84(geojson) + } + /** * Calculates the intersection between two features. * Returns the length if intersecting a linestring and a (multi)polygon (in meters), returns a surface area (in m²) if intersecting two (multi)polygons diff --git a/Models/ThemeConfig/Json/LayerConfigJson.ts b/Models/ThemeConfig/Json/LayerConfigJson.ts index e13734c8a0..c15c90d4a1 100644 --- a/Models/ThemeConfig/Json/LayerConfigJson.ts +++ b/Models/ThemeConfig/Json/LayerConfigJson.ts @@ -53,6 +53,8 @@ export interface LayerConfigJson { * source: {geoJson: "https://my.source.net/some-tile-geojson-{layer}-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14} * to use a tiled geojson source. The web server must offer multiple geojsons. {z}, {x} and {y} are substituted by the location; {layer} is substituted with the id of the loaded layer * + * Some API's use a BBOX instead of a tile, this can be used by specifying {y_min}, {y_max}, {x_min} and {x_max} + * Some API's use a mercator-projection (EPSG:900913) instead of WGS84. Set the flag `mercatorCrs: true` in the source for this * * Note that both geojson-options might set a flag 'isOsmCache' indicating that the data originally comes from OSM too * @@ -61,7 +63,7 @@ export interface LayerConfigJson { * While still supported, this is considered deprecated */ source: ({ osmTags: AndOrTagConfigJson | string, overpassScript?: string } | - { osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number, isOsmCache?: boolean }) & ({ + { osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number, isOsmCache?: boolean, mercatorCrs?: boolean }) & ({ /** * The maximum amount of seconds that a tile is allowed to linger in the cache */ diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index ba6ca94b8d..9f49a6c210 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -124,6 +124,7 @@ export default class LayerConfig { geojsonSourceLevel: json.source["geoJsonZoomLevel"], overpassScript: json.source["overpassScript"], isOsmCache: json.source["isOsmCache"], + mercatorCrs: json.source["mercatorCrs"] }, this.id ); diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index 1d89a104d7..54e2a64543 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -304,7 +304,6 @@ export default class LayoutConfig { } rewriting.forEach((value, key) => { console.log("Rewriting", key, "==>", value) - originalJson = originalJson.replace(new RegExp(key, "g"), value) }) return new LayoutConfig(JSON.parse(originalJson), false, "Layout rewriting") diff --git a/Models/ThemeConfig/SourceConfig.ts b/Models/ThemeConfig/SourceConfig.ts index 1d223bd2bd..b378d0acd3 100644 --- a/Models/ThemeConfig/SourceConfig.ts +++ b/Models/ThemeConfig/SourceConfig.ts @@ -1,4 +1,5 @@ import {TagsFilter} from "../../Logic/Tags/TagsFilter"; +import {RegexTag} from "../../Logic/Tags/RegexTag"; export default class SourceConfig { @@ -7,8 +8,10 @@ export default class SourceConfig { public readonly geojsonSource?: string; public readonly geojsonZoomLevel?: number; public readonly isOsmCacheLayer: boolean; + public readonly mercatorCrs: boolean; constructor(params: { + mercatorCrs?: boolean; osmTags?: TagsFilter, overpassScript?: string, geojsonSource?: string, @@ -33,10 +36,15 @@ export default class SourceConfig { console.error(params) throw `Source said it is a OSM-cached layer, but didn't define the actual source of the cache (in context ${context})` } - this.osmTags = params.osmTags; + if(params.geojsonSource !== undefined && params.geojsonSourceLevel !== undefined){ + if(! ["x","y","x_min","x_max","y_min","Y_max"].some(toSearch => params.geojsonSource.indexOf(toSearch) > 0)){ + throw `Source defines a geojson-zoomLevel, but does not specify {x} nor {y} (or equivalent), this is probably a bug (in context ${context})` + }} + this.osmTags = params.osmTags ?? new RegexTag("id",/.*/); this.overpassScript = params.overpassScript; this.geojsonSource = params.geojsonSource; this.geojsonZoomLevel = params.geojsonSourceLevel; this.isOsmCacheLayer = params.isOsmCache ?? false; + this.mercatorCrs = params.mercatorCrs ?? false; } } \ No newline at end of file diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index d46ebf711b..1f13262462 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -83,7 +83,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { layerConfig.allowMove ); }) - ) + ).SetClass("text-base") ); } @@ -94,14 +94,14 @@ export default class FeatureInfoBox extends ScrollableFullScreen { id, layerConfig.deletion )) - )) + ).SetClass("text-base")) } if (layerConfig.allowSplit) { editElements.push( new VariableUiElement(tags.map(tags => tags.id).map(id => new SplitRoadWizard(id)) - )) + ).SetClass("text-base")) } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 1882152965..680b3e571c 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -457,7 +457,7 @@ There are also some technicalities in your theme to keep in mind: return new Combine([new FixedUiElement("The import button is disabled for unofficial themes to prevent accidents.").SetClass("alert"), new FixedUiElement("To test, add 'test=true' to the URL. The changeset will be printed in the console. Please open a PR to officialize this theme to actually enable the import button.")]) } - const tgsSpec = args[0].split(",").map(spec => { + const tgsSpec = args[0].split(";").map(spec => { const kv = spec.split("=").map(s => s.trim()); if (kv.length != 2) { throw "Invalid key spec: multiple '=' found in " + spec diff --git a/assets/layers/public_bookcase/public_bookcase.json b/assets/layers/public_bookcase/public_bookcase.json index 407751fd95..b8b884c33d 100644 --- a/assets/layers/public_bookcase/public_bookcase.json +++ b/assets/layers/public_bookcase/public_bookcase.json @@ -1,490 +1,491 @@ { - "id": "public_bookcase", - "name": { - "en": "Bookcases", - "nl": "Boekenruilkastjes", - "de": "Bücherschränke", - "fr": "Microbibliothèque", - "ru": "Книжные шкафы", - "it": "Microbiblioteche" + "id": "public_bookcase", + "name": { + "en": "Bookcases", + "nl": "Boekenruilkastjes", + "de": "Bücherschränke", + "fr": "Microbibliothèque", + "ru": "Книжные шкафы", + "it": "Microbiblioteche" + }, + "description": { + "en": "A streetside cabinet with books, accessible to anyone", + "nl": "Een straatkastje met boeken voor iedereen", + "de": "Ein Bücherschrank am Straßenrand mit Büchern, für jedermann zugänglich", + "fr": "Une armoire ou une boite contenant des livres en libre accès", + "it": "Una vetrinetta ai bordi della strada contenente libri, aperta al pubblico", + "ru": "Уличный шкаф с книгами, доступными для всех" + }, + "source": { + "osmTags": "amenity=public_bookcase" + }, + "minzoom": 10, + "wayHandling": 2, + "title": { + "render": { + "en": "Bookcase", + "nl": "Boekenruilkast", + "de": "Bücherschrank", + "fr": "Microbibliothèque", + "ru": "Книжный шкаф", + "it": "Microbiblioteca" }, - "description": { - "en": "A streetside cabinet with books, accessible to anyone", - "nl": "Een straatkastje met boeken voor iedereen", - "de": "Ein Bücherschrank am Straßenrand mit Büchern, für jedermann zugänglich", - "fr": "Une armoire ou une boite contenant des livres en libre accès", - "it": "Una vetrinetta ai bordi della strada contenente libri, aperta al pubblico", - "ru": "Уличный шкаф с книгами, доступными для всех" - }, - "source": { - "osmTags": "amenity=public_bookcase" - }, - "minzoom": 10, - "wayHandling": 2, - "title": { - "render": { - "en": "Bookcase", - "nl": "Boekenruilkast", - "de": "Bücherschrank", - "fr": "Microbibliothèque", - "ru": "Книжный шкаф", - "it": "Microbiblioteca" - }, - "mappings": [ - { - "if": "name~*", - "then": { - "en": "Public bookcase {name}", - "nl": "Boekenruilkast {name}", - "de": "Öffentlicher Bücherschrank {name}", - "fr": "Microbibliothèque {name}", - "ru": "Общественный книжный шкаф {name}", - "it": "Microbiblioteca pubblica {name}" - } - } - ] - }, - "icon": { - "render": "./assets/themes/bookcases/bookcase.svg;" - }, - "label": { - "mappings": [ - { - "if": "name~*", - "then": "
{name}
" - } - ] - }, - "color": { - "render": "#0000ff" - }, - "width": { - "render": "8" - }, - "presets": [ - { - "title": { - "en": "Bookcase", - "nl": "Boekenruilkast", - "de": "Bücherschrank", - "fr": "Microbibliothèque", - "ru": "Книжный шкаф", - "it": "Microbiblioteca" - }, - "tags": [ - "amenity=public_bookcase" - ], - "preciseInput": { - "preferredBackground": "photo" - } - } - ], - "tagRenderings": [ - "images", - { - "id": "minimap", - "render": "{minimap():height: 9rem; border-radius: 2.5rem; overflow:hidden;border:1px solid gray}" - }, - { - "render": { - "en": "The name of this bookcase is {name}", - "nl": "De naam van dit boekenruilkastje is {name}", - "de": "Der Name dieses Bücherschrank lautet {name}", - "fr": "Le nom de cette microbibliothèque est {name}", - "ru": "Название книжного шкафа — {name}", - "it": "Questa microbiblioteca si chiama {name}" - }, - "question": { - "en": "What is the name of this public bookcase?", - "nl": "Wat is de naam van dit boekenuilkastje?", - "de": "Wie heißt dieser öffentliche Bücherschrank?", - "fr": "Quel est le nom de cette microbibliothèque ?", - "ru": "Как называется этот общественный книжный шкаф?", - "it": "Come si chiama questa microbiblioteca pubblica?" - }, - "freeform": { - "key": "name" - }, - "mappings": [ - { - "if": { - "and": [ - "noname=yes", - "name=" - ] - }, - "then": { - "en": "This bookcase doesn't have a name", - "nl": "Dit boekenruilkastje heeft geen naam", - "de": "Dieser Bücherschrank hat keinen Namen", - "fr": "Cette microbibliothèque n'a pas de nom", - "ru": "У этого книжного шкафа нет названия", - "it": "Questa microbiblioteca non ha un nome proprio" - } - } - ], - "id": "public_bookcase-name" - }, - { - "render": { - "en": "{capacity} books fit in this bookcase", - "nl": "Er passen {capacity} boeken", - "de": "{capacity} Bücher passen in diesen Bücherschrank", - "fr": "{capacity} livres peuvent entrer dans cette microbibliothèque", - "it": "Questa microbiblioteca può contenere fino a {capacity} libri", - "ru": "{capacity} книг помещается в этот книжный шкаф" - }, - "question": { - "en": "How many books fit into this public bookcase?", - "nl": "Hoeveel boeken passen er in dit boekenruilkastje?", - "de": "Wie viele Bücher passen in diesen öffentlichen Bücherschrank?", - "fr": "Combien de livres peuvent entrer dans cette microbibliothèque ?", - "ru": "Сколько книг помещается в этом общественном книжном шкафу?", - "it": "Quanti libri può contenere questa microbiblioteca?" - }, - "freeform": { - "key": "capacity", - "type": "nat", - "inline": true - }, - "id": "public_bookcase-capacity" - }, - { - "id": "bookcase-booktypes", - "question": { - "en": "What kind of books can be found in this public bookcase?", - "nl": "Voor welke doelgroep zijn de meeste boeken in dit boekenruilkastje?", - "de": "Welche Art von Büchern sind in diesem öffentlichen Bücherschrank zu finden?", - "fr": "Quel type de livres peut-on dans cette microbibliothèque ?", - "it": "Che tipo di libri si possono trovare in questa microbiblioteca?", - "ru": "Какие книги можно найти в этом общественном книжном шкафу?" - }, - "mappings": [ - { - "if": "books=children", - "then": { - "en": "Mostly children books", - "nl": "Voornamelijk kinderboeken", - "de": "Vorwiegend Kinderbücher", - "fr": "Livres pour enfants", - "ru": "В основном детские книги", - "it": "Principalmente libri per l'infanzia" - } - }, - { - "if": "books=adults", - "then": { - "en": "Mostly books for adults", - "nl": "Voornamelijk boeken voor volwassenen", - "de": "Vorwiegend Bücher für Erwachsene", - "fr": "Livres pour les adultes", - "ru": "В основном книги для взрослых", - "it": "Principalmente libri per persone in età adulta" - } - }, - { - "if": "books=children;adults", - "then": { - "en": "Both books for kids and adults", - "nl": "Boeken voor zowel kinderen als volwassenen", - "de": "Sowohl Bücher für Kinder als auch für Erwachsene", - "fr": "Livres pour enfants et adultes également", - "it": "Sia libri per l'infanzia, sia per l'età adulta", - "ru": "Книги и для детей, и для взрослых" - } - } - ] - }, - { - "id": "bookcase-is-indoors", - "question": { - "en": "Is this bookcase located outdoors?", - "nl": "Staat dit boekenruilkastje binnen of buiten?", - "de": "Befindet sich dieser Bücherschrank im Freien?", - "fr": "Cette microbiliothèque est-elle en extérieur ?", - "it": "Questa microbiblioteca si trova all'aperto?" - }, - "mappings": [ - { - "then": { - "en": "This bookcase is located indoors", - "nl": "Dit boekenruilkastje staat binnen", - "de": "Dieser Bücherschrank befindet sich im Innenbereich", - "fr": "Cette microbibliothèque est en intérieur", - "it": "Questa microbiblioteca si trova al chiuso" - }, - "if": "indoor=yes" - }, - { - "then": { - "en": "This bookcase is located outdoors", - "nl": "Dit boekenruilkastje staat buiten", - "de": "Dieser Bücherschrank befindet sich im Freien", - "fr": "Cette microbibliothèque est en extérieur", - "it": "Questa microbiblioteca si trova all'aperto" - }, - "if": "indoor=no" - }, - { - "then": { - "en": "This bookcase is located outdoors", - "nl": "Dit boekenruilkastje staat buiten", - "de": "Dieser Bücherschrank befindet sich im Freien", - "fr": "Cette microbibliothèque est en extérieur", - "it": "Questa microbiblioteca si trova all'aperto" - }, - "if": "indoor=", - "hideInAnswer": true - } - ] - }, - { - "id": "bookcase-is-accessible", - "question": { - "en": "Is this public bookcase freely accessible?", - "nl": "Is dit boekenruilkastje publiek toegankelijk?", - "de": "Ist dieser öffentliche Bücherschrank frei zugänglich?", - "fr": "Cette microbibliothèque est-elle librement accèssible ?", - "it": "Questa microbiblioteca è ad accesso libero?", - "ru": "Имеется ли свободный доступ к этому общественному книжному шкафу?" - }, - "condition": "indoor=yes", - "mappings": [ - { - "then": { - "en": "Publicly accessible", - "nl": "Publiek toegankelijk", - "de": "Öffentlich zugänglich", - "fr": "Accèssible au public", - "it": "È ad accesso libero", - "ru": "Свободный доступ" - }, - "if": "access=yes" - }, - { - "then": { - "en": "Only accessible to customers", - "nl": "Enkel toegankelijk voor klanten", - "de": "Nur für Kunden zugänglich", - "fr": "Accèssible aux clients", - "it": "L'accesso è riservato ai clienti" - }, - "if": "access=customers" - } - ] - }, - { - "question": { - "en": "Who maintains this public bookcase?", - "nl": "Wie is verantwoordelijk voor dit boekenruilkastje?", - "de": "Wer unterhält diesen öffentlichen Bücherschrank?", - "fr": "Qui entretien cette microbibliothèque ?", - "it": "Chi mantiene questa microbiblioteca?" - }, - "render": { - "en": "Operated by {operator}", - "nl": "Onderhouden door {operator}", - "de": "Betrieben von {operator}", - "fr": "Entretenue par {operator}", - "it": "È gestita da {operator}" - }, - "freeform": { - "type": "string", - "key": "operator" - }, - "id": "public_bookcase-operator" - }, - { - "question": { - "en": "Is this public bookcase part of a bigger network?", - "nl": "Is dit boekenruilkastje deel van een netwerk?", - "de": "Ist dieser öffentliche Bücherschrank Teil eines größeren Netzwerks?", - "fr": "Cette microbibliothèque fait-elle partie d'un réseau/groupe ?", - "it": "Questa microbiblioteca fa parte di una rete?" - }, - "render": { - "en": "This public bookcase is part of {brand}", - "nl": "Dit boekenruilkastje is deel van het netwerk {brand}", - "de": "Dieser Bücherschrank ist Teil von {brand}", - "fr": "Cette microbibliothèque fait partie du groupe {brand}", - "it": "Questa microbiblioteca fa parte di {brand}" - }, - "condition": "ref=", - "freeform": { - "key": "brand" - }, - "mappings": [ - { - "then": { - "en": "Part of the network 'Little Free Library'", - "nl": "Deel van het netwerk 'Little Free Library'", - "de": "Teil des Netzwerks 'Little Free Library'", - "fr": "Fait partie du réseau Little Free Library", - "it": "Fa parte della rete 'Little Free Library'" - }, - "if": { - "and": [ - "brand=Little Free Library", - "nobrand=" - ] - } - }, - { - "if": { - "and": [ - "nobrand=yes", - "brand=" - ] - }, - "then": { - "en": "This public bookcase is not part of a bigger network", - "nl": "Dit boekenruilkastje maakt geen deel uit van een netwerk", - "de": "Dieser öffentliche Bücherschrank ist nicht Teil eines größeren Netzwerks", - "fr": "Cette microbibliothèque ne fait pas partie d'un réseau/groupe", - "it": "Questa microbiblioteca non fa parte di una rete" - } - } - ], - "id": "public_bookcase-brand" - }, - { - "render": { - "en": "The reference number of this public bookcase within {brand} is {ref}", - "nl": "Het referentienummer binnen {brand} is {ref}", - "de": "Die Referenznummer dieses öffentlichen Bücherschranks innerhalb {brand} lautet {ref}", - "fr": "Cette microbibliothèque du réseau {brand} possède le numéro {ref}", - "it": "Il numero identificativo di questa microbiblioteca nella rete {brand} è {ref}" - }, - "question": { - "en": "What is the reference number of this public bookcase?", - "nl": "Wat is het referentienummer van dit boekenruilkastje?", - "de": "Wie lautet die Referenznummer dieses öffentlichen Bücherschranks?", - "fr": "Quelle est le numéro de référence de cette microbibliothèque ?", - "it": "Qual è il numero identificativo di questa microbiblioteca?" - }, - "condition": "brand~*", - "freeform": { - "key": "ref" - }, - "mappings": [ - { - "then": { - "en": "This bookcase is not part of a bigger network", - "nl": "Dit boekenruilkastje maakt geen deel uit van een netwerk", - "de": "Dieser Bücherschrank ist nicht Teil eines größeren Netzwerks", - "fr": "Cette microbibliothèque ne fait pas partie d'un réseau/groupe", - "it": "Questa microbiblioteca non fa parte di una rete" - }, - "if": { - "and": [ - "nobrand=yes", - "brand=", - "ref=" - ] - } - } - ], - "id": "public_bookcase-ref" - }, - { - "question": { - "en": "When was this public bookcase installed?", - "nl": "Op welke dag werd dit boekenruilkastje geinstalleerd?", - "de": "Wann wurde dieser öffentliche Bücherschrank installiert?", - "fr": "Quand a été installée cette microbibliothèque ?", - "it": "Quando è stata inaugurata questa microbiblioteca?", - "ru": "Когда был установлен этот общественный книжный шкаф?" - }, - "render": { - "en": "Installed on {start_date}", - "nl": "Geplaatst op {start_date}", - "de": "Installiert am {start_date}", - "fr": "Installée le {start_date}", - "it": "È stata inaugurata il {start_date}", - "ru": "Установлен {start_date}" - }, - "freeform": { - "key": "start_date", - "type": "date" - }, - "id": "public_bookcase-start_date" - }, - { - "render": { - "en": "More info on the website", - "nl": "Meer info op de website", - "de": "Weitere Informationen auf der Webseite", - "fr": "Plus d'infos sur le site web", - "ru": "Более подробная информация на сайте", - "it": "Maggiori informazioni sul sito web" - }, - "question": { - "en": "Is there a website with more information about this public bookcase?", - "nl": "Is er een website over dit boekenruilkastje?", - "de": "Gibt es eine Website mit weiteren Informationen über diesen öffentlichen Bücherschrank?", - "fr": "Y a-t-il un site web avec plus d'informations sur cette microbibliothèque ?", - "it": "C'è un sito web con maggiori informazioni su questa microbiblioteca?", - "ru": "Есть ли веб-сайт с более подробной информацией об этом общественном книжном шкафе?" - }, - "freeform": { - "key": "website", - "type": "url" - }, - "id": "public_bookcase-website" - } - ], - "deletion": { - "softDeletionTags": { - "and": [ - "disused:amenity=public_bookcase", - "amenity=" - ] - }, - "neededChangesets": 5 - }, - "filter": [ - { - "id": "kid-books", - "options": [ - { - "question": "Kinderboeken aanwezig?", - "osmTags": "books~.*children.*" - } - ] - }, - { - "id": "adult-books", - "options": [ - { - "question": "Boeken voor volwassenen aanwezig?", - "osmTags": "books~.*adults.*" - } - ] - }, - { - "id": "inside", - "options": [ - { - "question": { - "nl": "Binnen of buiten", - "en": "Indoor or outdoor", - "de": "Innen oder Außen" - } - }, - { - "question": "Binnen?", - "osmTags": "indoor=yes" - }, - { - "question": "Buiten?", - "osmTags": { - "or": [ - "indoor=no", - "indoor=" - ] - } - } - ] + "mappings": [ + { + "if": "name~*", + "then": { + "en": "Public bookcase {name}", + "nl": "Boekenruilkast {name}", + "de": "Öffentlicher Bücherschrank {name}", + "fr": "Microbibliothèque {name}", + "ru": "Общественный книжный шкаф {name}", + "it": "Microbiblioteca pubblica {name}" } + } ] + }, + "icon": { + "render": "./assets/themes/bookcases/bookcase.svg;" + }, + "label": { + "mappings": [ + { + "if": "name~*", + "then": "
{name}
" + } + ] + }, + "color": { + "render": "#0000ff" + }, + "width": { + "render": "8" + }, + "presets": [ + { + "title": { + "en": "Bookcase", + "nl": "Boekenruilkast", + "de": "Bücherschrank", + "fr": "Microbibliothèque", + "ru": "Книжный шкаф", + "it": "Microbiblioteca" + }, + "tags": [ + "amenity=public_bookcase" + ], + "preciseInput": { + "preferredBackground": "photo" + } + } + ], + "tagRenderings": [ + "images", + { + "id": "minimap", + "render": "{minimap():height: 9rem; border-radius: 2.5rem; overflow:hidden;border:1px solid gray}" + }, + { + "render": { + "en": "The name of this bookcase is {name}", + "nl": "De naam van dit boekenruilkastje is {name}", + "de": "Der Name dieses Bücherschrank lautet {name}", + "fr": "Le nom de cette microbibliothèque est {name}", + "ru": "Название книжного шкафа — {name}", + "it": "Questa microbiblioteca si chiama {name}" + }, + "question": { + "en": "What is the name of this public bookcase?", + "nl": "Wat is de naam van dit boekenuilkastje?", + "de": "Wie heißt dieser öffentliche Bücherschrank?", + "fr": "Quel est le nom de cette microbibliothèque ?", + "ru": "Как называется этот общественный книжный шкаф?", + "it": "Come si chiama questa microbiblioteca pubblica?" + }, + "freeform": { + "key": "name" + }, + "mappings": [ + { + "if": { + "and": [ + "noname=yes", + "name=" + ] + }, + "then": { + "en": "This bookcase doesn't have a name", + "nl": "Dit boekenruilkastje heeft geen naam", + "de": "Dieser Bücherschrank hat keinen Namen", + "fr": "Cette microbibliothèque n'a pas de nom", + "ru": "У этого книжного шкафа нет названия", + "it": "Questa microbiblioteca non ha un nome proprio" + } + } + ], + "id": "public_bookcase-name" + }, + { + "render": { + "en": "{capacity} books fit in this bookcase", + "nl": "Er passen {capacity} boeken", + "de": "{capacity} Bücher passen in diesen Bücherschrank", + "fr": "{capacity} livres peuvent entrer dans cette microbibliothèque", + "it": "Questa microbiblioteca può contenere fino a {capacity} libri", + "ru": "{capacity} книг помещается в этот книжный шкаф" + }, + "question": { + "en": "How many books fit into this public bookcase?", + "nl": "Hoeveel boeken passen er in dit boekenruilkastje?", + "de": "Wie viele Bücher passen in diesen öffentlichen Bücherschrank?", + "fr": "Combien de livres peuvent entrer dans cette microbibliothèque ?", + "ru": "Сколько книг помещается в этом общественном книжном шкафу?", + "it": "Quanti libri può contenere questa microbiblioteca?" + }, + "freeform": { + "key": "capacity", + "type": "nat", + "inline": true + }, + "id": "public_bookcase-capacity" + }, + { + "id": "bookcase-booktypes", + "question": { + "en": "What kind of books can be found in this public bookcase?", + "nl": "Voor welke doelgroep zijn de meeste boeken in dit boekenruilkastje?", + "de": "Welche Art von Büchern sind in diesem öffentlichen Bücherschrank zu finden?", + "fr": "Quel type de livres peut-on dans cette microbibliothèque ?", + "it": "Che tipo di libri si possono trovare in questa microbiblioteca?", + "ru": "Какие книги можно найти в этом общественном книжном шкафу?" + }, + "mappings": [ + { + "if": "books=children", + "then": { + "en": "Mostly children books", + "nl": "Voornamelijk kinderboeken", + "de": "Vorwiegend Kinderbücher", + "fr": "Livres pour enfants", + "ru": "В основном детские книги", + "it": "Principalmente libri per l'infanzia" + } + }, + { + "if": "books=adults", + "then": { + "en": "Mostly books for adults", + "nl": "Voornamelijk boeken voor volwassenen", + "de": "Vorwiegend Bücher für Erwachsene", + "fr": "Livres pour les adultes", + "ru": "В основном книги для взрослых", + "it": "Principalmente libri per persone in età adulta" + } + }, + { + "if": "books=children;adults", + "then": { + "en": "Both books for kids and adults", + "nl": "Boeken voor zowel kinderen als volwassenen", + "de": "Sowohl Bücher für Kinder als auch für Erwachsene", + "fr": "Livres pour enfants et adultes également", + "it": "Sia libri per l'infanzia, sia per l'età adulta", + "ru": "Книги и для детей, и для взрослых" + } + } + ] + }, + { + "id": "bookcase-is-indoors", + "question": { + "en": "Is this bookcase located outdoors?", + "nl": "Staat dit boekenruilkastje binnen of buiten?", + "de": "Befindet sich dieser Bücherschrank im Freien?", + "fr": "Cette microbiliothèque est-elle en extérieur ?", + "it": "Questa microbiblioteca si trova all'aperto?" + }, + "mappings": [ + { + "then": { + "en": "This bookcase is located indoors", + "nl": "Dit boekenruilkastje staat binnen", + "de": "Dieser Bücherschrank befindet sich im Innenbereich", + "fr": "Cette microbibliothèque est en intérieur", + "it": "Questa microbiblioteca si trova al chiuso" + }, + "if": "indoor=yes" + }, + { + "then": { + "en": "This bookcase is located outdoors", + "nl": "Dit boekenruilkastje staat buiten", + "de": "Dieser Bücherschrank befindet sich im Freien", + "fr": "Cette microbibliothèque est en extérieur", + "it": "Questa microbiblioteca si trova all'aperto" + }, + "if": "indoor=no" + }, + { + "then": { + "en": "This bookcase is located outdoors", + "nl": "Dit boekenruilkastje staat buiten", + "de": "Dieser Bücherschrank befindet sich im Freien", + "fr": "Cette microbibliothèque est en extérieur", + "it": "Questa microbiblioteca si trova all'aperto" + }, + "if": "indoor=", + "hideInAnswer": true + } + ] + }, + { + "id": "bookcase-is-accessible", + "question": { + "en": "Is this public bookcase freely accessible?", + "nl": "Is dit boekenruilkastje publiek toegankelijk?", + "de": "Ist dieser öffentliche Bücherschrank frei zugänglich?", + "fr": "Cette microbibliothèque est-elle librement accèssible ?", + "it": "Questa microbiblioteca è ad accesso libero?", + "ru": "Имеется ли свободный доступ к этому общественному книжному шкафу?" + }, + "condition": "indoor=yes", + "mappings": [ + { + "then": { + "en": "Publicly accessible", + "nl": "Publiek toegankelijk", + "de": "Öffentlich zugänglich", + "fr": "Accèssible au public", + "it": "È ad accesso libero", + "ru": "Свободный доступ" + }, + "if": "access=yes" + }, + { + "then": { + "en": "Only accessible to customers", + "nl": "Enkel toegankelijk voor klanten", + "de": "Nur für Kunden zugänglich", + "fr": "Accèssible aux clients", + "it": "L'accesso è riservato ai clienti" + }, + "if": "access=customers" + } + ] + }, + { + "question": { + "en": "Who maintains this public bookcase?", + "nl": "Wie is verantwoordelijk voor dit boekenruilkastje?", + "de": "Wer unterhält diesen öffentlichen Bücherschrank?", + "fr": "Qui entretien cette microbibliothèque ?", + "it": "Chi mantiene questa microbiblioteca?" + }, + "render": { + "en": "Operated by {operator}", + "nl": "Onderhouden door {operator}", + "de": "Betrieben von {operator}", + "fr": "Entretenue par {operator}", + "it": "È gestita da {operator}" + }, + "freeform": { + "type": "string", + "key": "operator" + }, + "id": "public_bookcase-operator" + }, + { + "question": { + "en": "Is this public bookcase part of a bigger network?", + "nl": "Is dit boekenruilkastje deel van een netwerk?", + "de": "Ist dieser öffentliche Bücherschrank Teil eines größeren Netzwerks?", + "fr": "Cette microbibliothèque fait-elle partie d'un réseau/groupe ?", + "it": "Questa microbiblioteca fa parte di una rete?" + }, + "render": { + "en": "This public bookcase is part of {brand}", + "nl": "Dit boekenruilkastje is deel van het netwerk {brand}", + "de": "Dieser Bücherschrank ist Teil von {brand}", + "fr": "Cette microbibliothèque fait partie du groupe {brand}", + "it": "Questa microbiblioteca fa parte di {brand}" + }, + "condition": "ref=", + "freeform": { + "key": "brand" + }, + "mappings": [ + { + "then": { + "en": "Part of the network 'Little Free Library'", + "nl": "Deel van het netwerk 'Little Free Library'", + "de": "Teil des Netzwerks 'Little Free Library'", + "fr": "Fait partie du réseau Little Free Library", + "it": "Fa parte della rete 'Little Free Library'" + }, + "if": { + "and": [ + "brand=Little Free Library", + "nobrand=" + ] + } + }, + { + "if": { + "and": [ + "nobrand=yes", + "brand=" + ] + }, + "then": { + "en": "This public bookcase is not part of a bigger network", + "nl": "Dit boekenruilkastje maakt geen deel uit van een netwerk", + "de": "Dieser öffentliche Bücherschrank ist nicht Teil eines größeren Netzwerks", + "fr": "Cette microbibliothèque ne fait pas partie d'un réseau/groupe", + "it": "Questa microbiblioteca non fa parte di una rete" + } + } + ], + "id": "public_bookcase-brand" + }, + { + "render": { + "en": "The reference number of this public bookcase within {brand} is {ref}", + "nl": "Het referentienummer binnen {brand} is {ref}", + "de": "Die Referenznummer dieses öffentlichen Bücherschranks innerhalb {brand} lautet {ref}", + "fr": "Cette microbibliothèque du réseau {brand} possède le numéro {ref}", + "it": "Il numero identificativo di questa microbiblioteca nella rete {brand} è {ref}" + }, + "question": { + "en": "What is the reference number of this public bookcase?", + "nl": "Wat is het referentienummer van dit boekenruilkastje?", + "de": "Wie lautet die Referenznummer dieses öffentlichen Bücherschranks?", + "fr": "Quelle est le numéro de référence de cette microbibliothèque ?", + "it": "Qual è il numero identificativo di questa microbiblioteca?" + }, + "condition": "brand~*", + "freeform": { + "key": "ref" + }, + "mappings": [ + { + "then": { + "en": "This bookcase is not part of a bigger network", + "nl": "Dit boekenruilkastje maakt geen deel uit van een netwerk", + "de": "Dieser Bücherschrank ist nicht Teil eines größeren Netzwerks", + "fr": "Cette microbibliothèque ne fait pas partie d'un réseau/groupe", + "it": "Questa microbiblioteca non fa parte di una rete" + }, + "if": { + "and": [ + "nobrand=yes", + "brand=", + "ref=" + ] + } + } + ], + "id": "public_bookcase-ref" + }, + { + "question": { + "en": "When was this public bookcase installed?", + "nl": "Op welke dag werd dit boekenruilkastje geinstalleerd?", + "de": "Wann wurde dieser öffentliche Bücherschrank installiert?", + "fr": "Quand a été installée cette microbibliothèque ?", + "it": "Quando è stata inaugurata questa microbiblioteca?", + "ru": "Когда был установлен этот общественный книжный шкаф?" + }, + "render": { + "en": "Installed on {start_date}", + "nl": "Geplaatst op {start_date}", + "de": "Installiert am {start_date}", + "fr": "Installée le {start_date}", + "it": "È stata inaugurata il {start_date}", + "ru": "Установлен {start_date}" + }, + "freeform": { + "key": "start_date", + "type": "date" + }, + "id": "public_bookcase-start_date" + }, + { + "render": { + "en": "More info on the website", + "nl": "Meer info op de website", + "de": "Weitere Informationen auf der Webseite", + "fr": "Plus d'infos sur le site web", + "ru": "Более подробная информация на сайте", + "it": "Maggiori informazioni sul sito web" + }, + "question": { + "en": "Is there a website with more information about this public bookcase?", + "nl": "Is er een website over dit boekenruilkastje?", + "de": "Gibt es eine Website mit weiteren Informationen über diesen öffentlichen Bücherschrank?", + "fr": "Y a-t-il un site web avec plus d'informations sur cette microbibliothèque ?", + "it": "C'è un sito web con maggiori informazioni su questa microbiblioteca?", + "ru": "Есть ли веб-сайт с более подробной информацией об этом общественном книжном шкафе?" + }, + "freeform": { + "key": "website", + "type": "url" + }, + "id": "public_bookcase-website" + } + ], + "allowMove": true, + "deletion": { + "softDeletionTags": { + "and": [ + "disused:amenity=public_bookcase", + "amenity=" + ] + }, + "neededChangesets": 5 + }, + "filter": [ + { + "id": "kid-books", + "options": [ + { + "question": "Kinderboeken aanwezig?", + "osmTags": "books~.*children.*" + } + ] + }, + { + "id": "adult-books", + "options": [ + { + "question": "Boeken voor volwassenen aanwezig?", + "osmTags": "books~.*adults.*" + } + ] + }, + { + "id": "inside", + "options": [ + { + "question": { + "nl": "Binnen of buiten", + "en": "Indoor or outdoor", + "de": "Innen oder Außen" + } + }, + { + "question": "Binnen?", + "osmTags": "indoor=yes" + }, + { + "question": "Buiten?", + "osmTags": { + "or": [ + "indoor=no", + "indoor=" + ] + } + } + ] + } + ] } \ No newline at end of file diff --git a/assets/themes/grb_import/README.md b/assets/themes/grb_import/README.md new file mode 100644 index 0000000000..2ed7bfbbfa --- /dev/null +++ b/assets/themes/grb_import/README.md @@ -0,0 +1,20 @@ + GRB Import helper +=================== + + +Preparing the CRAB dataset +-------------------------- + +```` +# The original data is downloaded from https://download.vlaanderen.be/Producten/Detail?id=447&title=CRAB_Adressenlijst# (the GML-file here ) +wget https://downloadagiv.blob.core.windows.net/crab-adressenlijst/GML/CRAB_Adressenlijst_GML.zip + +# Extract the zip file +unzip CRAB_Adressenlijst_GML.zip + +# convert the pesky GML file into geojson +ogr2ogr -progress -t_srs WGS84 -f \"GeoJson\" CRAB.geojson CrabAdr.gml + +# When done, this big file is sliced into tiles with the slicer script +node --max_old_space_size=8000 $(which ts-node) ~/git/MapComplete/scripts/slice.ts CRAB.geojson 18 ~/git/pietervdvn.github.io/CRAB_2021_10_26 +```` \ No newline at end of file diff --git a/assets/themes/grb.json b/assets/themes/grb_import/grb.json similarity index 80% rename from assets/themes/grb.json rename to assets/themes/grb_import/grb.json index 9c62526175..4f0d99d9db 100644 --- a/assets/themes/grb.json +++ b/assets/themes/grb_import/grb.json @@ -22,7 +22,7 @@ "socialImage": "", "layers": [ { - "id": "grb-fixmes", + "id": "osm-fixmes", "name": { "nl": "Fixmes op gebouwen" }, @@ -198,6 +198,43 @@ }, "wayHandling": 2, "presets": [] + }, + { + "id": "crab-addresses 2021-10-26", + "source": { + "osmTags": "HUISNR~*", + "geoJson": "https://raw.githubusercontent.com/pietervdvn/pietervdvn.github.io/master/CRAB_2021_10_26/tile_{z}_{x}_{y}.geojson", + "#geoJson": "https://pietervdvn.github.io/CRAB_2021_10_26/tile_{z}_{x}_{y}.geojson", + "geoJsonZoomLevel": 18, + "maxCacheAge": 0 + }, + "minzoom": 19, + "name": "CRAB-addressen", + "title": "CRAB-adres", + "icon": "circle:#bb3322", + "iconSize": "15,15,center", + "tagRenderings": [ + "all_tags", + { + "id": "import-button", + "render": "{import_button(addr:street=$STRAATNM; addr:housenumber=$HUISNR)}" + } + ] + }, + { + "id": "GRB", + "source": { + "geoJson": "https://betadata.grbosm.site/grb?bbox={x_min},{y_min},{x_max},{y_max}", + "geoJsonZoomLevel": 18, + "mercatorCrs": true, + "maxCacheAge": 0 + }, + "name": "GRB geometries", + "title": "GRB outline", + "minzoom": 19, + "tagRenderings": [ + "all_tags" + ] } ], "hideFromOverview": true, diff --git a/index.ts b/index.ts index 92ba9da66b..a383eaac26 100644 --- a/index.ts +++ b/index.ts @@ -33,8 +33,6 @@ if (location.href.startsWith("http://buurtnatuur.be")) { class Init { - - public static Init(layoutToUse: LayoutConfig, encoded: string) { if(layoutToUse === null){ diff --git a/scripts/slice.ts b/scripts/slice.ts index d7ae5e186e..74673035b5 100644 --- a/scripts/slice.ts +++ b/scripts/slice.ts @@ -4,11 +4,84 @@ import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSou import * as readline from "readline"; import ScriptUtils from "./ScriptUtils"; +async function readFeaturesFromLineDelimitedJsonFile(inputFile: string): Promise { + const fileStream = fs.createReadStream(inputFile); + + const rl = readline.createInterface({ + input: fileStream, + crlfDelay: Infinity + }); + // Note: we use the crlfDelay option to recognize all instances of CR LF + // ('\r\n') in input.txt as a single line break. + + const allFeatures: any[] = [] + // @ts-ignore + for await (const line of rl) { + try { + allFeatures.push(JSON.parse(line)) + } catch (e) { + console.error("Could not parse", line) + break + } + if (allFeatures.length % 10000 === 0) { + ScriptUtils.erasableLog("Loaded ", allFeatures.length, "features up till now") + } + } + return allFeatures +} + +async function readGeojsonLineByLine(inputFile: string): Promise { + const fileStream = fs.createReadStream(inputFile); + + const rl = readline.createInterface({ + input: fileStream, + crlfDelay: Infinity + }); + // Note: we use the crlfDelay option to recognize all instances of CR LF + // ('\r\n') in input.txt as a single line break. + + const allFeatures: any[] = [] + let featuresSeen = false + // @ts-ignore + for await (let line: string of rl) { + if (!featuresSeen && line.startsWith("\"features\":")) { + featuresSeen = true; + continue; + } + if (!featuresSeen) { + continue + } + if (line.endsWith(",")) { + line = line.substring(0, line.length - 1) + } + + try { + allFeatures.push(JSON.parse(line)) + } catch (e) { + console.error("Could not parse", line) + break + } + if (allFeatures.length % 10000 === 0) { + ScriptUtils.erasableLog("Loaded ", allFeatures.length, "features up till now") + } + } + return allFeatures +} + +async function readFeaturesFromGeoJson(inputFile: string): Promise { + try { + return JSON.parse(fs.readFileSync(inputFile, "UTF-8")).features + } catch (e) { + // We retry, but with a line-by-line approach + return await readGeojsonLineByLine(inputFile) + } +} + async function main(args: string[]) { console.log("GeoJSON slicer") if (args.length < 3) { - console.log("USAGE: ") + console.log("USAGE: ") return } @@ -23,39 +96,24 @@ async function main(args: string[]) { console.log("Using directory ", outputDirectory) - const fileStream = fs.createReadStream(inputFile); - - const rl = readline.createInterface({ - input: fileStream, - crlfDelay: Infinity - }); - // Note: we use the crlfDelay option to recognize all instances of CR LF - // ('\r\n') in input.txt as a single line break. - - const allFeatures = [] - // @ts-ignore - for await (const line of rl) { - // Each line in input.txt will be successively available here as `line`. - try{ - allFeatures.push(JSON.parse(line)) - }catch (e) { - console.error("Could not parse", line) - break - } - if(allFeatures.length % 10000 === 0){ - ScriptUtils.erasableLog("Loaded ", allFeatures.length, "features up till now") - } + let allFeatures: any []; + if (inputFile.endsWith(".geojson")) { + allFeatures = await readFeaturesFromGeoJson(inputFile) + } else { + allFeatures = await readFeaturesFromLineDelimitedJsonFile(inputFile) } - + + console.log("Loaded all", allFeatures.length, "points") - - const keysToRemove = ["ID","STRAATNMID","NISCODE","GEMEENTE","POSTCODE","HERKOMST","APPTNR"] + + const keysToRemove = ["ID", "STRAATNMID", "NISCODE", "GEMEENTE", "POSTCODE", "HERKOMST"] for (const f of allFeatures) { for (const keyToRm of keysToRemove) { delete f.properties[keyToRm] } + delete f.bbox } - + //const knownKeys = Utils.Dedup([].concat(...allFeatures.map(f => Object.keys(f.properties)))) //console.log("Kept keys: ", knownKeys) @@ -67,11 +125,15 @@ async function main(args: string[]) { maxFeatureCount: Number.MAX_VALUE, registerTile: tile => { const path = `${outputDirectory}/tile_${tile.z}_${tile.x}_${tile.y}.geojson` + const features = tile.features.data.map(ff => ff.feature) + features.forEach(f => { + delete f.bbox + }) fs.writeFileSync(path, JSON.stringify({ "type": "FeatureCollection", - "features": tile.features.data.map(ff => ff.feature) + "features": features }, null, " ")) - console.log("Written ", path, "which has ", tile.features.data.length, "features") + ScriptUtils.erasableLog("Written ", path, "which has ", tile.features.data.length, "features") } } )