Merge branch 'develop'
This commit is contained in:
commit
f5f4bc7fde
17 changed files with 751 additions and 562 deletions
|
@ -1,5 +1,6 @@
|
||||||
import * as turf from "@turf/turf";
|
import * as turf from "@turf/turf";
|
||||||
import {TileRange, Tiles} from "../Models/TileRange";
|
import {TileRange, Tiles} from "../Models/TileRange";
|
||||||
|
import {GeoOperations} from "./GeoOperations";
|
||||||
|
|
||||||
export class BBox {
|
export class BBox {
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ export class BBox {
|
||||||
this.minLon = Math.min(this.minLon, coordinate[0]);
|
this.minLon = Math.min(this.minLon, coordinate[0]);
|
||||||
this.minLat = Math.min(this.minLat, coordinate[1]);
|
this.minLat = Math.min(this.minLat, coordinate[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.maxLon = Math.min(this.maxLon, 180)
|
this.maxLon = Math.min(this.maxLon, 180)
|
||||||
this.maxLat = Math.min(this.maxLat, 90)
|
this.maxLat = Math.min(this.maxLat, 90)
|
||||||
this.minLon = Math.max(this.minLon, -180)
|
this.minLon = Math.max(this.minLon, -180)
|
||||||
|
@ -117,12 +118,12 @@ export class BBox {
|
||||||
}
|
}
|
||||||
|
|
||||||
pad(factor: number, maxIncrease = 2): BBox {
|
pad(factor: number, maxIncrease = 2): BBox {
|
||||||
|
|
||||||
const latDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLat - this.minLat) * factor)
|
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([[
|
return new BBox([[
|
||||||
this.minLon - lonDiff,
|
this.minLon - lonDiff,
|
||||||
this.minLat - latDiff
|
this.minLat - latDiff
|
||||||
], [this.maxLon + lonDiff,
|
], [this.maxLon + lonDiff,
|
||||||
this.maxLat + latDiff]])
|
this.maxLat + latDiff]])
|
||||||
}
|
}
|
||||||
|
@ -161,4 +162,16 @@ export class BBox {
|
||||||
const boundslr = Tiles.tile_bounds_lon_lat(lr.z, lr.x, lr.y)
|
const boundslr = Tiles.tile_bounds_lon_lat(lr.z, lr.x, lr.y)
|
||||||
return new BBox([].concat(boundsul, boundslr))
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -18,7 +18,6 @@ export default class DetermineLayout {
|
||||||
*/
|
*/
|
||||||
public static async GetLayout(): Promise<[LayoutConfig, string]> {
|
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 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);
|
const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data);
|
||||||
|
|
||||||
|
@ -73,17 +72,13 @@ export default class DetermineLayout {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const data = await Utils.downloadJson(link)
|
const parsed = await Utils.downloadJson(link)
|
||||||
|
console.log("Got ", parsed)
|
||||||
try {
|
try {
|
||||||
let parsed = data;
|
|
||||||
if (typeof parsed == "string") {
|
|
||||||
parsed = JSON.parse(parsed);
|
|
||||||
}
|
|
||||||
// Overwrite the id to the url
|
|
||||||
parsed.id = link;
|
parsed.id = link;
|
||||||
return new LayoutConfig(parsed, false).patchImages(link, data);
|
return new LayoutConfig(parsed, false).patchImages(link, JSON.stringify(parsed));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
DetermineLayout.ShowErrorOnCustomTheme(
|
DetermineLayout.ShowErrorOnCustomTheme(
|
||||||
`<a href="${link}">${link}</a> is invalid:`,
|
`<a href="${link}">${link}</a> is invalid:`,
|
||||||
new FixedUiElement(e)
|
new FixedUiElement(e)
|
||||||
|
@ -92,6 +87,7 @@ export default class DetermineLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.erorr(e)
|
||||||
DetermineLayout.ShowErrorOnCustomTheme(
|
DetermineLayout.ShowErrorOnCustomTheme(
|
||||||
`<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
|
`<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
|
||||||
new FixedUiElement(e)
|
new FixedUiElement(e)
|
||||||
|
@ -107,7 +103,7 @@ export default class DetermineLayout {
|
||||||
try {
|
try {
|
||||||
// layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
|
// layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
|
||||||
const dedicatedHashFromLocalStorage = LocalStorageSource.Get(
|
const dedicatedHashFromLocalStorage = LocalStorageSource.Get(
|
||||||
"user-layout-" + userLayoutParam.data.replace(" ", "_")
|
"user-layout-" + userLayoutParam.data?.replace(" ", "_")
|
||||||
);
|
);
|
||||||
if (dedicatedHashFromLocalStorage.data?.length < 10) {
|
if (dedicatedHashFromLocalStorage.data?.length < 10) {
|
||||||
dedicatedHashFromLocalStorage.setData(undefined);
|
dedicatedHashFromLocalStorage.setData(undefined);
|
||||||
|
@ -134,6 +130,7 @@ export default class DetermineLayout {
|
||||||
try {
|
try {
|
||||||
json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash)))
|
json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash)))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON"))
|
DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON"))
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -143,6 +140,7 @@ export default class DetermineLayout {
|
||||||
userLayoutParam.setData(layoutToUse.id);
|
userLayoutParam.setData(layoutToUse.id);
|
||||||
return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))];
|
return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
if (hash === undefined || hash.length < 10) {
|
if (hash === undefined || hash.length < 10) {
|
||||||
DetermineLayout.ShowErrorOnCustomTheme("Could not load a theme from the hash", new FixedUiElement("Hash does not contain data"))
|
DetermineLayout.ShowErrorOnCustomTheme("Could not load a theme from the hash", new FixedUiElement("Hash does not contain data"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,7 @@ export default class FeaturePipeline {
|
||||||
this.osmSourceZoomLevel = state.osmApiTileSize.data;
|
this.osmSourceZoomLevel = state.osmApiTileSize.data;
|
||||||
const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12))
|
const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12))
|
||||||
this.relationTracker = new RelationsTracker()
|
this.relationTracker = new RelationsTracker()
|
||||||
|
|
||||||
state.changes.allChanges.addCallbackAndRun(allChanges => {
|
state.changes.allChanges.addCallbackAndRun(allChanges => {
|
||||||
allChanges.filter(ch => ch.id < 0 && ch.changes !== undefined)
|
allChanges.filter(ch => ch.id < 0 && ch.changes !== undefined)
|
||||||
.map(ch => ch.changes)
|
.map(ch => ch.changes)
|
||||||
|
@ -205,7 +205,9 @@ export default class FeaturePipeline {
|
||||||
neededTiles: neededTilesFromOsm,
|
neededTiles: neededTilesFromOsm,
|
||||||
handleTile: tile => {
|
handleTile: tile => {
|
||||||
new RegisteringAllFromFeatureSourceActor(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)
|
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
|
||||||
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
|
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
|
||||||
|
|
||||||
|
@ -213,7 +215,9 @@ export default class FeaturePipeline {
|
||||||
state: state,
|
state: state,
|
||||||
markTileVisited: (tileId) =>
|
markTileVisited: (tileId) =>
|
||||||
state.filteredLayers.data.forEach(flayer => {
|
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())
|
self.freshnesses.get(flayer.layerDef.id).addTileLoad(tileId, new Date())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {Utils} from "../../../Utils";
|
||||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||||
import {Tiles} from "../../../Models/TileRange";
|
import {Tiles} from "../../../Models/TileRange";
|
||||||
import {BBox} from "../../BBox";
|
import {BBox} from "../../BBox";
|
||||||
|
import {GeoOperations} from "../../GeoOperations";
|
||||||
|
|
||||||
|
|
||||||
export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
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 features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||||
public readonly name;
|
public readonly name;
|
||||||
public readonly isOsmCache: boolean
|
public readonly isOsmCache: boolean
|
||||||
private onFail: ((errorMsg: any, url: string) => void) = undefined;
|
|
||||||
private readonly seenids: Set<string> = new Set<string>()
|
private readonly seenids: Set<string> = new Set<string>()
|
||||||
public readonly layer: FilteredLayer;
|
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);
|
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
|
||||||
if (zxy !== undefined) {
|
if (zxy !== undefined) {
|
||||||
const [z, x, y] = zxy;
|
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
|
url = url
|
||||||
.replace('{z}', "" + z)
|
.replace('{z}', "" + z)
|
||||||
.replace('{x}', "" + x)
|
.replace('{x}', "" + x)
|
||||||
.replace('{y}', "" + y)
|
.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.tileIndex = Tiles.tile_index(z, x, y)
|
||||||
this.bbox = BBox.fromTile(z, x, y)
|
this.bbox = BBox.fromTile(z, x, y)
|
||||||
} else {
|
} else {
|
||||||
|
@ -71,6 +81,10 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
||||||
if(json.features === undefined || json.features === null){
|
if(json.features === undefined || json.features === null){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(self.layer.layerDef.source.mercatorCrs){
|
||||||
|
json = GeoOperations.GeoJsonToWGS84(json)
|
||||||
|
}
|
||||||
|
|
||||||
const time = new Date();
|
const time = new Date();
|
||||||
const newFeatures: { feature: any, freshness: Date } [] = []
|
const newFeatures: { feature: any, freshness: Date } [] = []
|
||||||
|
|
|
@ -20,24 +20,28 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
||||||
if (source.geojsonSource === undefined) {
|
if (source.geojsonSource === undefined) {
|
||||||
throw "Invalid layer: geojsonSource expected"
|
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
|
let whitelist = undefined
|
||||||
Utils.downloadJson(whitelistUrl).then(
|
if (source.geojsonSource.indexOf("{x}_{y}.geojson") > 0) {
|
||||||
json => {
|
|
||||||
const data = new Map<number, Set<number>>();
|
const whitelistUrl = source.geojsonSource
|
||||||
for (const x in json) {
|
.replace("{z}", "" + source.geojsonZoomLevel)
|
||||||
data.set(Number(x), new Set(json[x]))
|
.replace("{x}_{y}.geojson", "overview.json")
|
||||||
|
.replace("{layer}", layer.layerDef.id)
|
||||||
|
|
||||||
|
Utils.downloadJson(whitelistUrl).then(
|
||||||
|
json => {
|
||||||
|
const data = new Map<number, Set<number>>();
|
||||||
|
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<string>();
|
const seenIds = new Set<string>();
|
||||||
const blackList = new UIEventSource(seenIds)
|
const blackList = new UIEventSource(seenIds)
|
||||||
|
@ -45,14 +49,14 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
||||||
layer,
|
layer,
|
||||||
source.geojsonZoomLevel,
|
source.geojsonZoomLevel,
|
||||||
(zxy) => {
|
(zxy) => {
|
||||||
if(whitelist !== undefined){
|
if (whitelist !== undefined) {
|
||||||
const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2])
|
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")
|
console.log("Not downloading tile", ...zxy, "as it is not on the whitelist")
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const src = new GeoJsonSource(
|
const src = new GeoJsonSource(
|
||||||
layer,
|
layer,
|
||||||
zxy,
|
zxy,
|
||||||
|
|
|
@ -226,7 +226,7 @@ export class GeoOperations {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the closest point on a way from a given point
|
* Generates the closest point on a way from a given point
|
||||||
*
|
*
|
||||||
* The properties object will contain three values:
|
* The properties object will contain three values:
|
||||||
// - `index`: closest point was found on nth line part,
|
// - `index`: closest point was found on nth line part,
|
||||||
// - `dist`: distance between pt and the closest point (in kilometer),
|
// - `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")
|
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.
|
* 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
|
* Returns the length if intersecting a linestring and a (multi)polygon (in meters), returns a surface area (in m²) if intersecting two (multi)polygons
|
||||||
|
|
|
@ -53,6 +53,8 @@ export interface LayerConfigJson {
|
||||||
* source: {geoJson: "https://my.source.net/some-tile-geojson-{layer}-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14}
|
* 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
|
* 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
|
* 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
|
* While still supported, this is considered deprecated
|
||||||
*/
|
*/
|
||||||
source: ({ osmTags: AndOrTagConfigJson | string, overpassScript?: string } |
|
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
|
* The maximum amount of seconds that a tile is allowed to linger in the cache
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -124,6 +124,7 @@ export default class LayerConfig {
|
||||||
geojsonSourceLevel: json.source["geoJsonZoomLevel"],
|
geojsonSourceLevel: json.source["geoJsonZoomLevel"],
|
||||||
overpassScript: json.source["overpassScript"],
|
overpassScript: json.source["overpassScript"],
|
||||||
isOsmCache: json.source["isOsmCache"],
|
isOsmCache: json.source["isOsmCache"],
|
||||||
|
mercatorCrs: json.source["mercatorCrs"]
|
||||||
},
|
},
|
||||||
this.id
|
this.id
|
||||||
);
|
);
|
||||||
|
|
|
@ -304,7 +304,6 @@ export default class LayoutConfig {
|
||||||
}
|
}
|
||||||
rewriting.forEach((value, key) => {
|
rewriting.forEach((value, key) => {
|
||||||
console.log("Rewriting", key, "==>", value)
|
console.log("Rewriting", key, "==>", value)
|
||||||
|
|
||||||
originalJson = originalJson.replace(new RegExp(key, "g"), value)
|
originalJson = originalJson.replace(new RegExp(key, "g"), value)
|
||||||
})
|
})
|
||||||
return new LayoutConfig(JSON.parse(originalJson), false, "Layout rewriting")
|
return new LayoutConfig(JSON.parse(originalJson), false, "Layout rewriting")
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||||
|
import {RegexTag} from "../../Logic/Tags/RegexTag";
|
||||||
|
|
||||||
export default class SourceConfig {
|
export default class SourceConfig {
|
||||||
|
|
||||||
|
@ -7,8 +8,10 @@ export default class SourceConfig {
|
||||||
public readonly geojsonSource?: string;
|
public readonly geojsonSource?: string;
|
||||||
public readonly geojsonZoomLevel?: number;
|
public readonly geojsonZoomLevel?: number;
|
||||||
public readonly isOsmCacheLayer: boolean;
|
public readonly isOsmCacheLayer: boolean;
|
||||||
|
public readonly mercatorCrs: boolean;
|
||||||
|
|
||||||
constructor(params: {
|
constructor(params: {
|
||||||
|
mercatorCrs?: boolean;
|
||||||
osmTags?: TagsFilter,
|
osmTags?: TagsFilter,
|
||||||
overpassScript?: string,
|
overpassScript?: string,
|
||||||
geojsonSource?: string,
|
geojsonSource?: string,
|
||||||
|
@ -33,10 +36,15 @@ export default class SourceConfig {
|
||||||
console.error(params)
|
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})`
|
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.overpassScript = params.overpassScript;
|
||||||
this.geojsonSource = params.geojsonSource;
|
this.geojsonSource = params.geojsonSource;
|
||||||
this.geojsonZoomLevel = params.geojsonSourceLevel;
|
this.geojsonZoomLevel = params.geojsonSourceLevel;
|
||||||
this.isOsmCacheLayer = params.isOsmCache ?? false;
|
this.isOsmCacheLayer = params.isOsmCache ?? false;
|
||||||
|
this.mercatorCrs = params.mercatorCrs ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -83,7 +83,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
||||||
layerConfig.allowMove
|
layerConfig.allowMove
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)
|
).SetClass("text-base")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,14 +94,14 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
||||||
id,
|
id,
|
||||||
layerConfig.deletion
|
layerConfig.deletion
|
||||||
))
|
))
|
||||||
))
|
).SetClass("text-base"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layerConfig.allowSplit) {
|
if (layerConfig.allowSplit) {
|
||||||
editElements.push(
|
editElements.push(
|
||||||
new VariableUiElement(tags.map(tags => tags.id).map(id =>
|
new VariableUiElement(tags.map(tags => tags.id).map(id =>
|
||||||
new SplitRoadWizard(id))
|
new SplitRoadWizard(id))
|
||||||
))
|
).SetClass("text-base"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"),
|
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.")])
|
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());
|
const kv = spec.split("=").map(s => s.trim());
|
||||||
if (kv.length != 2) {
|
if (kv.length != 2) {
|
||||||
throw "Invalid key spec: multiple '=' found in " + spec
|
throw "Invalid key spec: multiple '=' found in " + spec
|
||||||
|
|
|
@ -1,490 +1,491 @@
|
||||||
{
|
{
|
||||||
"id": "public_bookcase",
|
"id": "public_bookcase",
|
||||||
"name": {
|
"name": {
|
||||||
"en": "Bookcases",
|
"en": "Bookcases",
|
||||||
"nl": "Boekenruilkastjes",
|
"nl": "Boekenruilkastjes",
|
||||||
"de": "Bücherschränke",
|
"de": "Bücherschränke",
|
||||||
"fr": "Microbibliothèque",
|
"fr": "Microbibliothèque",
|
||||||
"ru": "Книжные шкафы",
|
"ru": "Книжные шкафы",
|
||||||
"it": "Microbiblioteche"
|
"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": {
|
"mappings": [
|
||||||
"en": "A streetside cabinet with books, accessible to anyone",
|
{
|
||||||
"nl": "Een straatkastje met boeken voor iedereen",
|
"if": "name~*",
|
||||||
"de": "Ein Bücherschrank am Straßenrand mit Büchern, für jedermann zugänglich",
|
"then": {
|
||||||
"fr": "Une armoire ou une boite contenant des livres en libre accès",
|
"en": "Public bookcase <i>{name}</i>",
|
||||||
"it": "Una vetrinetta ai bordi della strada contenente libri, aperta al pubblico",
|
"nl": "Boekenruilkast <i>{name}</i>",
|
||||||
"ru": "Уличный шкаф с книгами, доступными для всех"
|
"de": "Öffentlicher Bücherschrank <i>{name}</i>",
|
||||||
},
|
"fr": "Microbibliothèque <i>{name}</i>",
|
||||||
"source": {
|
"ru": "Общественный книжный шкаф <i>{name}</i>",
|
||||||
"osmTags": "amenity=public_bookcase"
|
"it": "Microbiblioteca pubblica <i>{name}</i>"
|
||||||
},
|
|
||||||
"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 <i>{name}</i>",
|
|
||||||
"nl": "Boekenruilkast <i>{name}</i>",
|
|
||||||
"de": "Öffentlicher Bücherschrank <i>{name}</i>",
|
|
||||||
"fr": "Microbibliothèque <i>{name}</i>",
|
|
||||||
"ru": "Общественный книжный шкаф <i>{name}</i>",
|
|
||||||
"it": "Microbiblioteca pubblica <i>{name}</i>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"render": "./assets/themes/bookcases/bookcase.svg;"
|
|
||||||
},
|
|
||||||
"label": {
|
|
||||||
"mappings": [
|
|
||||||
{
|
|
||||||
"if": "name~*",
|
|
||||||
"then": "<div style='background: white; padding: 0.25em; border-radius:0.5em'>{name}</div>"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"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 <a href='{website}' target='_blank'>the website</a>",
|
|
||||||
"nl": "Meer info op <a href='{website}' target='_blank'>de website</a>",
|
|
||||||
"de": "Weitere Informationen auf <a href='{website}' target='_blank'>der Webseite</a>",
|
|
||||||
"fr": "Plus d'infos sur <a href='{website}' target='_blank'>le site web</a>",
|
|
||||||
"ru": "Более подробная информация <a href='{website}' target='_blank'>на сайте</a>",
|
|
||||||
"it": "Maggiori informazioni sul <a href='{website}' target='_blank'>sito web</a>"
|
|
||||||
},
|
|
||||||
"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="
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"render": "./assets/themes/bookcases/bookcase.svg;"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "name~*",
|
||||||
|
"then": "<div style='background: white; padding: 0.25em; border-radius:0.5em'>{name}</div>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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 <a href='{website}' target='_blank'>the website</a>",
|
||||||
|
"nl": "Meer info op <a href='{website}' target='_blank'>de website</a>",
|
||||||
|
"de": "Weitere Informationen auf <a href='{website}' target='_blank'>der Webseite</a>",
|
||||||
|
"fr": "Plus d'infos sur <a href='{website}' target='_blank'>le site web</a>",
|
||||||
|
"ru": "Более подробная информация <a href='{website}' target='_blank'>на сайте</a>",
|
||||||
|
"it": "Maggiori informazioni sul <a href='{website}' target='_blank'>sito web</a>"
|
||||||
|
},
|
||||||
|
"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="
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
20
assets/themes/grb_import/README.md
Normal file
20
assets/themes/grb_import/README.md
Normal file
|
@ -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
|
||||||
|
````
|
|
@ -22,7 +22,7 @@
|
||||||
"socialImage": "",
|
"socialImage": "",
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
"id": "grb-fixmes",
|
"id": "osm-fixmes",
|
||||||
"name": {
|
"name": {
|
||||||
"nl": "Fixmes op gebouwen"
|
"nl": "Fixmes op gebouwen"
|
||||||
},
|
},
|
||||||
|
@ -198,6 +198,43 @@
|
||||||
},
|
},
|
||||||
"wayHandling": 2,
|
"wayHandling": 2,
|
||||||
"presets": []
|
"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,
|
"hideFromOverview": true,
|
2
index.ts
2
index.ts
|
@ -33,8 +33,6 @@ if (location.href.startsWith("http://buurtnatuur.be")) {
|
||||||
|
|
||||||
|
|
||||||
class Init {
|
class Init {
|
||||||
|
|
||||||
|
|
||||||
public static Init(layoutToUse: LayoutConfig, encoded: string) {
|
public static Init(layoutToUse: LayoutConfig, encoded: string) {
|
||||||
|
|
||||||
if(layoutToUse === null){
|
if(layoutToUse === null){
|
||||||
|
|
120
scripts/slice.ts
120
scripts/slice.ts
|
@ -4,11 +4,84 @@ import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSou
|
||||||
import * as readline from "readline";
|
import * as readline from "readline";
|
||||||
import ScriptUtils from "./ScriptUtils";
|
import ScriptUtils from "./ScriptUtils";
|
||||||
|
|
||||||
|
async function readFeaturesFromLineDelimitedJsonFile(inputFile: string): Promise<any[]> {
|
||||||
|
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<any[]> {
|
||||||
|
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<any[]> {
|
||||||
|
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[]) {
|
async function main(args: string[]) {
|
||||||
|
|
||||||
console.log("GeoJSON slicer")
|
console.log("GeoJSON slicer")
|
||||||
if (args.length < 3) {
|
if (args.length < 3) {
|
||||||
console.log("USAGE: <input-file.line-delimited-geojson> <target-zoom-level> <output-directory>")
|
console.log("USAGE: <input-file.geojson> <target-zoom-level> <output-directory>")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,39 +96,24 @@ async function main(args: string[]) {
|
||||||
console.log("Using directory ", outputDirectory)
|
console.log("Using directory ", outputDirectory)
|
||||||
|
|
||||||
|
|
||||||
const fileStream = fs.createReadStream(inputFile);
|
let allFeatures: any [];
|
||||||
|
if (inputFile.endsWith(".geojson")) {
|
||||||
const rl = readline.createInterface({
|
allFeatures = await readFeaturesFromGeoJson(inputFile)
|
||||||
input: fileStream,
|
} else {
|
||||||
crlfDelay: Infinity
|
allFeatures = await readFeaturesFromLineDelimitedJsonFile(inputFile)
|
||||||
});
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
console.log("Loaded all", allFeatures.length, "points")
|
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 f of allFeatures) {
|
||||||
for (const keyToRm of keysToRemove) {
|
for (const keyToRm of keysToRemove) {
|
||||||
delete f.properties[keyToRm]
|
delete f.properties[keyToRm]
|
||||||
}
|
}
|
||||||
|
delete f.bbox
|
||||||
}
|
}
|
||||||
|
|
||||||
//const knownKeys = Utils.Dedup([].concat(...allFeatures.map(f => Object.keys(f.properties))))
|
//const knownKeys = Utils.Dedup([].concat(...allFeatures.map(f => Object.keys(f.properties))))
|
||||||
//console.log("Kept keys: ", knownKeys)
|
//console.log("Kept keys: ", knownKeys)
|
||||||
|
|
||||||
|
@ -67,11 +125,15 @@ async function main(args: string[]) {
|
||||||
maxFeatureCount: Number.MAX_VALUE,
|
maxFeatureCount: Number.MAX_VALUE,
|
||||||
registerTile: tile => {
|
registerTile: tile => {
|
||||||
const path = `${outputDirectory}/tile_${tile.z}_${tile.x}_${tile.y}.geojson`
|
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({
|
fs.writeFileSync(path, JSON.stringify({
|
||||||
"type": "FeatureCollection",
|
"type": "FeatureCollection",
|
||||||
"features": tile.features.data.map(ff => ff.feature)
|
"features": features
|
||||||
}, null, " "))
|
}, null, " "))
|
||||||
console.log("Written ", path, "which has ", tile.features.data.length, "features")
|
ScriptUtils.erasableLog("Written ", path, "which has ", tile.features.data.length, "features")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue