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")
}
}
)