Add possibility to use a cutom overpass script, add 'grassfields in parks'-layer
This commit is contained in:
parent
0d51015cc8
commit
f659bc1141
40 changed files with 499 additions and 222 deletions
|
@ -28,6 +28,8 @@ import * as sport_pitch from "../assets/layers/sport_pitch/sport_pitch.json"
|
|||
import * as slow_roads from "../assets/layers/slow_roads/slow_roads.json"
|
||||
import LayerConfig from "./JSON/LayerConfig";
|
||||
import {LayerConfigJson} from "./JSON/LayerConfigJson";
|
||||
import * as grass_in_parks from "../assets/layers/village_green/grass_in_parks.json"
|
||||
import * as village_green from "../assets/layers/village_green/village_green.json"
|
||||
|
||||
export default class AllKnownLayers {
|
||||
|
||||
|
@ -60,7 +62,9 @@ export default class AllKnownLayers {
|
|||
play_forest,
|
||||
playground,
|
||||
sport_pitch,
|
||||
slow_roads
|
||||
slow_roads,
|
||||
grass_in_parks,
|
||||
village_green
|
||||
];
|
||||
|
||||
// Must be below the list...
|
||||
|
|
|
@ -9,7 +9,6 @@ export class FromJSON {
|
|||
const tag = Utils.SplitFirst(json, "=");
|
||||
return new Tag(tag[0], tag[1]);
|
||||
}
|
||||
|
||||
public static Tag(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter {
|
||||
try{
|
||||
return this.TagUnsafe(json, context);
|
||||
|
|
|
@ -15,6 +15,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||
import {UIElement} from "../../UI/UIElement";
|
||||
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
|
||||
import SourceConfig from "./SourceConfig";
|
||||
|
||||
export default class LayerConfig {
|
||||
|
||||
|
@ -25,7 +26,7 @@ export default class LayerConfig {
|
|||
id: string;
|
||||
name: Translation
|
||||
description: Translation;
|
||||
overpassTags: TagsFilter;
|
||||
source: SourceConfig;
|
||||
doNotDownload: boolean;
|
||||
passAllFeatures: boolean;
|
||||
minzoom: number;
|
||||
|
@ -49,8 +50,6 @@ export default class LayerConfig {
|
|||
|
||||
tagRenderings: TagRenderingConfig [];
|
||||
|
||||
private readonly configuration_warnings: string[] = []
|
||||
|
||||
constructor(json: LayerConfigJson,
|
||||
context?: string) {
|
||||
context = context + "." + json.id;
|
||||
|
@ -58,9 +57,39 @@ export default class LayerConfig {
|
|||
this.id = json.id;
|
||||
this.name = Translations.T(json.name, context + ".name");
|
||||
this.description = Translations.T(json.description, context + ".description");
|
||||
this.overpassTags = FromJSON.Tag(json.overpassTags, context + ".overpasstags");
|
||||
this.doNotDownload = json.doNotDownload ?? false,
|
||||
this.passAllFeatures = json.passAllFeatures ?? false;
|
||||
|
||||
let legacy = undefined;
|
||||
if (json["overpassTags"] !== undefined) {
|
||||
// @ts-ignore
|
||||
legacy = FromJSON.Tag(json["overpassTags"], context + ".overpasstags");
|
||||
}
|
||||
if(json.source !== undefined){
|
||||
if (legacy !== undefined ) {
|
||||
throw context+"Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined"
|
||||
}
|
||||
|
||||
let osmTags: TagsFilter = legacy;
|
||||
if (json.source["osmTags"]) {
|
||||
osmTags = FromJSON.Tag(json.source["osmTags"], context + "source.osmTags");
|
||||
}
|
||||
|
||||
|
||||
this.source = new SourceConfig({
|
||||
osmTags: osmTags,
|
||||
geojsonSource: json.source["geoJsonSource"],
|
||||
overpassScript: json.source["overpassScript"],
|
||||
});
|
||||
}else{
|
||||
this.source = new SourceConfig({
|
||||
osmTags : legacy
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
this.doNotDownload = json.doNotDownload ?? false;
|
||||
this.passAllFeatures = json.passAllFeatures ?? false;
|
||||
this.minzoom = json.minzoom;
|
||||
this.wayHandling = json.wayHandling ?? 0;
|
||||
this.hideUnderlayingFeaturesMinPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0;
|
||||
|
@ -82,16 +111,15 @@ export default class LayerConfig {
|
|||
if (deflt === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return new TagRenderingConfig(deflt, self.overpassTags, `${context}.${key}.default value`);
|
||||
return new TagRenderingConfig(deflt, self.source.osmTags, `${context}.${key}.default value`);
|
||||
}
|
||||
if (typeof v === "string") {
|
||||
const shared = SharedTagRenderings.SharedTagRendering[v];
|
||||
if (shared) {
|
||||
console.log("Got shared TR:", v, "-->", shared)
|
||||
return shared;
|
||||
}
|
||||
}
|
||||
return new TagRenderingConfig(v, self.overpassTags, `${context}.${key}`);
|
||||
return new TagRenderingConfig(v, self.source.osmTags, `${context}.${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -119,7 +147,7 @@ export default class LayerConfig {
|
|||
}
|
||||
throw `Predefined tagRendering ${renderingJson} not found in ${context}`;
|
||||
}
|
||||
return new TagRenderingConfig(renderingJson, self.overpassTags, `${context}.tagrendering[${i}]`);
|
||||
return new TagRenderingConfig(renderingJson, self.source.osmTags, `${context}.tagrendering[${i}]`);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -142,7 +170,7 @@ export default class LayerConfig {
|
|||
this.title = tr("title", undefined);
|
||||
this.icon = tr("icon", Img.AsData(Svg.pin));
|
||||
this.iconOverlays = (json.iconOverlays ?? []).map((overlay, i) => {
|
||||
let tr = new TagRenderingConfig(overlay.then, self.overpassTags, `iconoverlays.${i}`);
|
||||
let tr = new TagRenderingConfig(overlay.then, self.source.osmTags, `iconoverlays.${i}`);
|
||||
if (typeof overlay.then === "string" && SharedTagRenderings.SharedIcons[overlay.then] !== undefined) {
|
||||
tr = SharedTagRenderings.SharedIcons[overlay.then];
|
||||
}
|
||||
|
@ -281,7 +309,7 @@ export default class LayerConfig {
|
|||
|
||||
const iconUrlStatic = render(this.icon);
|
||||
const self = this;
|
||||
var mappedHtml = tags.map(tgs => {
|
||||
const mappedHtml = tags.map(tgs => {
|
||||
// What do you mean, 'tgs' is never read?
|
||||
// It is read implicitly in the 'render' method
|
||||
const iconUrl = render(self.icon);
|
||||
|
|
|
@ -26,8 +26,23 @@ export interface LayerConfigJson {
|
|||
|
||||
/**
|
||||
* The tags to load from overpass. Either a simple 'key=value'-string, otherwise an advanced configuration
|
||||
* DEPRECATED
|
||||
* shorthand for source: {osmTags: "key=value"}
|
||||
*/
|
||||
overpassTags: AndOrTagConfigJson | string;
|
||||
//overpassTags: AndOrTagConfigJson | string;
|
||||
|
||||
/**
|
||||
* This determines where the data for the layer is fetched.
|
||||
* There are some options:
|
||||
*
|
||||
* source: {osmTags: "key=value"} will fetch all objects with given tags from OSM. Currently, this will create a query to overpass and fetch the data - in the future this might fetch from the OSM API
|
||||
* source: {geoJsonSource: "https://my.source.net/some-geo-data.geojson"} to fetch a geojson from a third party source
|
||||
*
|
||||
* source: {overpassScript: "<custom overpass tags>"} when you want to do special things. _This should be really rare_.
|
||||
* This means that the data will be pulled from overpass with this script, and will ignore the osmTags for the query
|
||||
* However, for the rest of the pipeline, the OsmTags will _still_ be used. This is important to enable layers etc...
|
||||
*/
|
||||
source: {osmTags: AndOrTagConfigJson | string} | {geoJsonSource: string} | {overpassScript: string}
|
||||
|
||||
/**
|
||||
* If set, this layer will not query overpass; but it'll still match the tags above which are by chance returned by other layers.
|
||||
|
|
32
Customizations/JSON/SourceConfig.ts
Normal file
32
Customizations/JSON/SourceConfig.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import {TagsFilter} from "../../Logic/Tags";
|
||||
|
||||
export default class SourceConfig {
|
||||
|
||||
osmTags?: TagsFilter;
|
||||
overpassScript?: string;
|
||||
geojsonSource?: string;
|
||||
|
||||
constructor(params: {
|
||||
osmTags?: TagsFilter,
|
||||
overpassScript?: string,
|
||||
geojsonSource?: string
|
||||
}) {
|
||||
|
||||
let defined = 0;
|
||||
if (params.osmTags) {
|
||||
defined++;
|
||||
}
|
||||
if (params.overpassScript) {
|
||||
defined++;
|
||||
}
|
||||
if (params.geojsonSource) {
|
||||
defined++;
|
||||
}
|
||||
if (defined == 0) {
|
||||
throw "Source: nothing correct defined in the source"
|
||||
}
|
||||
this.osmTags = params.osmTags;
|
||||
this.overpassScript = params.overpassScript;
|
||||
this.geojsonSource = params.geojsonSource;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
|||
import {Overpass} from "../Osm/Overpass";
|
||||
import Bounds from "../../Models/Bounds";
|
||||
import FeatureSource from "../FeatureSource/FeatureSource";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
|
||||
export default class UpdateFromOverpass implements FeatureSource {
|
||||
|
@ -71,11 +72,12 @@ export default class UpdateFromOverpass implements FeatureSource {
|
|||
this.update();
|
||||
}
|
||||
|
||||
private GetFilter() {
|
||||
const filters: TagsFilter[] = [];
|
||||
private GetFilter(): Overpass {
|
||||
let filters: TagsFilter[] = [];
|
||||
let extraScripts: string[] = [];
|
||||
for (const layer of this._layoutToUse.data.layers) {
|
||||
if (typeof (layer) === "string") {
|
||||
continue;
|
||||
throw "A layer was not expanded!"
|
||||
}
|
||||
if (this._location.data.zoom < layer.minzoom) {
|
||||
continue;
|
||||
|
@ -102,20 +104,21 @@ export default class UpdateFromOverpass implements FeatureSource {
|
|||
if (previouslyLoaded) {
|
||||
continue;
|
||||
}
|
||||
filters.push(layer.overpassTags);
|
||||
if (layer.source.overpassScript !== undefined) {
|
||||
extraScripts.push(layer.source.overpassScript)
|
||||
} else {
|
||||
filters.push(layer.source.osmTags);
|
||||
}
|
||||
}
|
||||
if (filters.length === 0) {
|
||||
filters = Utils.NoNull(filters)
|
||||
extraScripts = Utils.NoNull(extraScripts)
|
||||
if (filters.length + extraScripts.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return new Or(filters);
|
||||
return new Overpass(new Or(filters), extraScripts);
|
||||
}
|
||||
|
||||
private update(): void {
|
||||
const filter = this.GetFilter();
|
||||
if (filter === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.runningQuery.data) {
|
||||
console.log("Still running a query, skip");
|
||||
return;
|
||||
|
@ -133,9 +136,12 @@ export default class UpdateFromOverpass implements FeatureSource {
|
|||
|
||||
const z = Math.floor(this._location.data.zoom ?? 0);
|
||||
|
||||
this.runningQuery.setData(true);
|
||||
const self = this;
|
||||
const overpass = new Overpass(filter);
|
||||
const overpass = this.GetFilter();
|
||||
if (overpass === undefined) {
|
||||
return;
|
||||
}
|
||||
this.runningQuery.setData(true);
|
||||
overpass.queryGeoJson(queryBounds,
|
||||
function (data, date) {
|
||||
self._previousBounds.get(z).push(queryBounds);
|
||||
|
@ -165,9 +171,10 @@ export default class UpdateFromOverpass implements FeatureSource {
|
|||
self.timeout.setData(0);
|
||||
self.update()
|
||||
}
|
||||
}, 1000
|
||||
}, 1000
|
||||
)
|
||||
}
|
||||
|
||||
countDown();
|
||||
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ export default class FeatureDuplicatorPerLayer implements FeatureSource {
|
|||
|
||||
let foundALayer = false;
|
||||
for (const layer of layers.data) {
|
||||
if (layer.layerDef.overpassTags.matchesProperties(f.feature.properties)) {
|
||||
if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) {
|
||||
foundALayer = true;
|
||||
if (layer.layerDef.passAllFeatures) {
|
||||
|
||||
|
|
|
@ -9,9 +9,11 @@ import Bounds from "../../Models/Bounds";
|
|||
export class Overpass {
|
||||
private _filter: TagsFilter
|
||||
public static testUrl: string = null
|
||||
private readonly _extraScripts: string[];
|
||||
|
||||
constructor(filter: TagsFilter) {
|
||||
constructor(filter: TagsFilter, extraScripts: string[]) {
|
||||
this._filter = filter
|
||||
this._extraScripts = extraScripts;
|
||||
}
|
||||
|
||||
|
||||
|
@ -21,6 +23,9 @@ export class Overpass {
|
|||
for (const filterOr of filters) {
|
||||
filter += 'nwr' + filterOr + ';'
|
||||
}
|
||||
for (const extraScript of this._extraScripts){
|
||||
filter += '('+extraScript+');';
|
||||
}
|
||||
const query =
|
||||
'[out:json][timeout:25]' + bbox + ';(' + filter + ');out body;>;out skel qt;'
|
||||
return "https://overpass-api.de/api/interpreter?data=" + encodeURIComponent(query)
|
||||
|
@ -48,6 +53,7 @@ export class Overpass {
|
|||
}
|
||||
// @ts-ignore
|
||||
const geojson = OsmToGeoJson.default(json);
|
||||
console.log("Received geojson", geojson)
|
||||
const osmTime = new Date(json.osm3s.timestamp_osm_base);
|
||||
continuation(geojson, osmTime);
|
||||
}).fail(onFail)
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Utils } from "../Utils";
|
|||
|
||||
export default class Constants {
|
||||
|
||||
public static vNumber = "0.5.14";
|
||||
public static vNumber = "0.6.0";
|
||||
|
||||
// The user journey states thresholds when a new feature gets unlocked
|
||||
public static userJourney = {
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
"fr": "Bancs"
|
||||
},
|
||||
"minzoom": 14,
|
||||
"overpassTags": "amenity=bench",
|
||||
"source": {
|
||||
"osmTags": "amenity=bench"
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Bench",
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
"fr": "Bancs des arrêts de transport en commun"
|
||||
},
|
||||
"minzoom": 14,
|
||||
"overpassTags": {
|
||||
"or": [
|
||||
"bench=yes",
|
||||
"bench=stand_up_bench"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"bench=yes",
|
||||
"bench=stand_up_bench"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
"nl": "Picnictafels"
|
||||
},
|
||||
"minzoom": 12,
|
||||
"overpassTags": "leisure=picnic_table",
|
||||
"source": {
|
||||
"osmTags": "leisure=picnic_table"
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Picnic table",
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"nl": "Fietsbibliotheek"
|
||||
},
|
||||
"minzoom": 8 ,
|
||||
"overpassTags": "amenity=bicycle_library",
|
||||
"source": {
|
||||
"osmTags": "amenity=bicycle_library"},
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Bicycle library",
|
||||
|
|
|
@ -37,11 +37,13 @@
|
|||
}
|
||||
],
|
||||
"iconSize": "50,50,bottom",
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"amenity=vending_machine",
|
||||
"vending~.*bicycle_tube.*"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"amenity=vending_machine",
|
||||
"vending~.*bicycle_tube.*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"minzoom": 13,
|
||||
"wayHandling": 2,
|
||||
|
@ -104,7 +106,7 @@
|
|||
"mappings": [
|
||||
{
|
||||
"if": "payment:coins=yes",
|
||||
"ifnot": "payment:coins=no",
|
||||
"ifnot": "payment:coins=no",
|
||||
"then": "Payment with coins is possible"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -8,18 +8,20 @@
|
|||
"de": "Fahrrad-Café"
|
||||
},
|
||||
"minzoom": 13,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"amenity~pub|bar|cafe|restaurant",
|
||||
{
|
||||
"#": "Note the double tilde in 'service:bicycle' which interprets the key as regex too",
|
||||
"or": [
|
||||
"pub~cycling|bicycle",
|
||||
"theme~cycling|bicycle",
|
||||
"service:bicycle:.*~~*"
|
||||
]
|
||||
}
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"amenity~pub|bar|cafe|restaurant",
|
||||
{
|
||||
"#": "Note the double tilde in 'service:bicycle' which interprets the key as regex too",
|
||||
"or": [
|
||||
"pub~cycling|bicycle",
|
||||
"theme~cycling|bicycle",
|
||||
"service:bicycle:.*~~*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
|
|
@ -23,12 +23,14 @@
|
|||
"render": "./assets/layers/bike_cleaning/bike_cleaning.svg"
|
||||
},
|
||||
"iconSize": "50,50,bottom",
|
||||
"overpassTags": {
|
||||
"or": [
|
||||
"service:bicycle:cleaning=yes",
|
||||
"service:bicycle:cleaning=diy",
|
||||
"amenity=bicycle_wash"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"service:bicycle:cleaning=yes",
|
||||
"service:bicycle:cleaning=diy",
|
||||
"amenity=bicycle_wash"
|
||||
]
|
||||
}
|
||||
},
|
||||
"minzoom": 13,
|
||||
"wayHandling": 1,
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
{
|
||||
{
|
||||
"id": "bike_monitoring_station",
|
||||
"name": {
|
||||
"en": "Monitoring stations"
|
||||
},
|
||||
"minzoom": 12,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"man_made=monitoring_station",
|
||||
"monitoring:bicycle=yes"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"man_made=monitoring_station",
|
||||
"monitoring:bicycle=yes"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
@ -32,12 +34,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"tagRenderings": [ "images",
|
||||
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"render": "<b>{live({url},{url:format},hour)}</b> cyclists last hour<br/><b>{live({url},{url:format},day)}</b> cyclists today<br/><b>{live({url},{url:format},year)}</b> cyclists this year<br/>",
|
||||
"condition": {
|
||||
"and": ["url~*","url:format~*"]
|
||||
"and": [
|
||||
"url~*",
|
||||
"url:format~*"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
"de": "Fahrrad-Parkplätze"
|
||||
},
|
||||
"minzoom": 17,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"amenity=bicycle_parking"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"amenity=bicycle_parking"
|
||||
]
|
||||
}
|
||||
},
|
||||
"icon": {
|
||||
"render": {
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
"de": "Fahrradstationen (Reparatur, Pumpe oder beides)"
|
||||
},
|
||||
"minzoom": 13,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"amenity=bicycle_repair_station"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"amenity=bicycle_repair_station"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
@ -71,7 +73,8 @@
|
|||
"gl": "Bomba de ar estragada",
|
||||
"de": "Kaputte Pumpe"
|
||||
}
|
||||
},{
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"service:bicycle:pump=yes",
|
||||
|
@ -107,7 +110,7 @@
|
|||
"titleIcons": [
|
||||
{
|
||||
"render": "<a href='https://fietsambassade.gent.be/' target='_blank'><img src='./assets/themes/cyclofix/fietsambassade_gent_logo_small.svg'/></a>",
|
||||
"condition": "operator=De Fietsambassade Gent",
|
||||
"condition": "operator=De Fietsambassade Gent",
|
||||
"roaming": true
|
||||
},
|
||||
"defaults"
|
||||
|
|
|
@ -8,41 +8,43 @@
|
|||
"de": "Fahrradwerkstatt/geschäft"
|
||||
},
|
||||
"minzoom": 13,
|
||||
"overpassTags": {
|
||||
"#": "We select all bicycle shops, sport shops (but we try to weed out non-bicycle related shops), and any shop with a bicycle related tag",
|
||||
"or": [
|
||||
"shop=bicycle",
|
||||
{
|
||||
"#": "A bicycle rental with a network is something such as villo, bluebike, ... We don't want them",
|
||||
"and": [
|
||||
"amenity=bicycle_rental",
|
||||
"network="
|
||||
]
|
||||
},
|
||||
{
|
||||
"#": "if sport is defined and is not bicycle, it is retrackted; if bicycle retail/repair is marked as 'no', it is retracted too.",
|
||||
"##": "There will be a few false-positives with this. They will get filtered out by people marking both 'not selling bikes' and 'not repairing bikes'. Furthermore, the OSMers will add a sports-subcategory on it",
|
||||
"and": [
|
||||
"shop=sports",
|
||||
"service:bicycle:retail!=no",
|
||||
"service:bicycle:repair!=no",
|
||||
{
|
||||
"or": [
|
||||
"sport=bicycle",
|
||||
"sport=cycling",
|
||||
"sport="
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"#": "Any shop with any bicycle service",
|
||||
"and": [
|
||||
"shop~*",
|
||||
"service:bicycle:.*~~.*"
|
||||
]
|
||||
}
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"#": "We select all bicycle shops, sport shops (but we try to weed out non-bicycle related shops), and any shop with a bicycle related tag",
|
||||
"or": [
|
||||
"shop=bicycle",
|
||||
{
|
||||
"#": "A bicycle rental with a network is something such as villo, bluebike, ... We don't want them",
|
||||
"and": [
|
||||
"amenity=bicycle_rental",
|
||||
"network="
|
||||
]
|
||||
},
|
||||
{
|
||||
"#": "if sport is defined and is not bicycle, it is retrackted; if bicycle retail/repair is marked as 'no', it is retracted too.",
|
||||
"##": "There will be a few false-positives with this. They will get filtered out by people marking both 'not selling bikes' and 'not repairing bikes'. Furthermore, the OSMers will add a sports-subcategory on it",
|
||||
"and": [
|
||||
"shop=sports",
|
||||
"service:bicycle:retail!=no",
|
||||
"service:bicycle:repair!=no",
|
||||
{
|
||||
"or": [
|
||||
"sport=bicycle",
|
||||
"sport=cycling",
|
||||
"sport="
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"#": "Any shop with any bicycle service",
|
||||
"and": [
|
||||
"shop~*",
|
||||
"service:bicycle:.*~~.*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
@ -575,7 +577,7 @@
|
|||
"badge": true
|
||||
},
|
||||
{
|
||||
"if": "service:bicycle:pump=yes",
|
||||
"if": "service:bicycle:pump=yes",
|
||||
"then": "circle:#e2783d;./assets/layers/bike_repair_station/pump.svg",
|
||||
"badge": true
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@
|
|||
"nl": "Vogelkijkhutten"
|
||||
},
|
||||
"minzoom": 14,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"leisure=bird_hide"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"leisure=bird_hide"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
|
|
@ -7,14 +7,16 @@
|
|||
"de": "Mit Fahrrad zusammenhängendes Objekt"
|
||||
},
|
||||
"minzoom": 13,
|
||||
"overpassTags": {
|
||||
"or": [
|
||||
"theme~cycling|bicycle",
|
||||
"sport=cycling",
|
||||
"association~cycling|bicycle",
|
||||
"ngo~cycling|bicycle",
|
||||
"club~bicycle|cycling"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"theme~cycling|bicycle",
|
||||
"sport=cycling",
|
||||
"association~cycling|bicycle",
|
||||
"ngo~cycling|bicycle",
|
||||
"club~bicycle|cycling"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
"en": "Direction visualization"
|
||||
},
|
||||
"minzoom": 16,
|
||||
"overpassTags": {
|
||||
"or": [
|
||||
"camera:direction~*",
|
||||
"direction~*"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"camera:direction~*",
|
||||
"direction~*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"doNotDownload": true,
|
||||
"passAllFeatures": true,
|
||||
|
|
|
@ -31,14 +31,15 @@
|
|||
"badge": true
|
||||
}
|
||||
],
|
||||
|
||||
"iconSize": "40,40,bottom",
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"amenity=drinking_water",
|
||||
"access!=permissive",
|
||||
"access!=private"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"amenity=drinking_water",
|
||||
"access!=permissive",
|
||||
"access!=private"
|
||||
]
|
||||
}
|
||||
},
|
||||
"minzoom": 13,
|
||||
"wayHandling": 1,
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
"nl": "Witte Fietsen",
|
||||
"de": "Geisterrad"
|
||||
},
|
||||
"overpassTags": "memorial=ghost_bike",
|
||||
"source": {
|
||||
"osmTags": "memorial=ghost_bike"
|
||||
},
|
||||
"minzoom": 0,
|
||||
"title": {
|
||||
"render": {
|
||||
|
@ -43,7 +45,7 @@
|
|||
}
|
||||
],
|
||||
"tagRenderings": [
|
||||
{
|
||||
{
|
||||
"render": {
|
||||
"en": "A <b>ghost bike</b> is a memorial for a cyclist who died in a traffic accident, in the form of a white bicycle placed permanently near the accident location.",
|
||||
"nl": "Een Witte Fiets (of Spookfiets) is een aandenken aan een fietser die bij een verkeersongeval om het leven kwam. Het gaat over een witgeschilderde fiets die geplaatst werd in de buurt van het ongeval.",
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
"en": "Information boards"
|
||||
},
|
||||
"minzoom": 12,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"information=board"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"information=board"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
@ -16,7 +18,9 @@
|
|||
"en": "Information board"
|
||||
}
|
||||
},
|
||||
"tagRenderings": [ "images"],
|
||||
"tagRenderings": [
|
||||
"images"
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 0,
|
||||
"icon": {
|
||||
"render": "./assets/layers/information_board/board.svg"
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
"nl": "Kaarten"
|
||||
},
|
||||
"minzoom": 12,
|
||||
"overpassTags": {
|
||||
"or": [
|
||||
"tourism=map",
|
||||
"information=map"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"tourism=map",
|
||||
"information=map"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
"nl": "Natuurgebied"
|
||||
},
|
||||
"minzoom": 12,
|
||||
"overpassTags": {
|
||||
"or": [
|
||||
"leisure=nature_reserve",
|
||||
"boundary=protected_area"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"leisure=nature_reserve",
|
||||
"boundary=protected_area"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
@ -330,7 +332,8 @@
|
|||
"key": "description:0"
|
||||
}
|
||||
},
|
||||
{"#": "Surface are",
|
||||
{
|
||||
"#": "Surface are",
|
||||
"render": {
|
||||
"en": "Surface area: {_surface:ha}Ha",
|
||||
"mappings": {
|
||||
|
|
|
@ -4,10 +4,12 @@
|
|||
"nl": "Speelbossen"
|
||||
},
|
||||
"minzoom": 13,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"playground=forest"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"playground=forest"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
"en": "Playgrounds"
|
||||
},
|
||||
"minzoom": 13,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"leisure=playground",
|
||||
"playground!=forest"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"leisure=playground",
|
||||
"playground!=forest"
|
||||
]
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"nl": "Speeltuinen",
|
||||
|
@ -299,7 +301,7 @@
|
|||
"render": "https://upload.wikimedia.org/wikipedia/commons/0/00/Map_icons_by_Scott_de_Jonge_-_playground.svg"
|
||||
},
|
||||
"width": {
|
||||
"render": "3"
|
||||
"render": "1"
|
||||
},
|
||||
"iconSize": {
|
||||
"render": "40,40,center"
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
"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"
|
||||
},
|
||||
"overpassTags": "amenity=public_bookcase",
|
||||
"source": {
|
||||
"osmTags": "amenity=public_bookcase"
|
||||
},
|
||||
"minzoom": 12,
|
||||
"wayHandling": 2,
|
||||
"title": {
|
||||
|
@ -256,9 +258,12 @@
|
|||
"de": "Teil des Netzwerks 'Little Free Library'",
|
||||
"fr": "Fait partie du réseau 'Little Free Library'"
|
||||
},
|
||||
"if":{
|
||||
"and": ["brand=Little Free Library","nobrand="]
|
||||
}
|
||||
"if": {
|
||||
"and": [
|
||||
"brand=Little Free Library",
|
||||
"nobrand="
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
|
|
|
@ -4,16 +4,18 @@
|
|||
"nl": "Trage wegen"
|
||||
},
|
||||
"minzoom": 14,
|
||||
"overpassTags": {
|
||||
"or": [
|
||||
"highway=pedestrian",
|
||||
"highway=cycleway",
|
||||
"highway=footway",
|
||||
"highway=path",
|
||||
"highway=bridleway",
|
||||
"highway=living_street",
|
||||
"highway=track"
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"highway=pedestrian",
|
||||
"highway=cycleway",
|
||||
"highway=footway",
|
||||
"highway=path",
|
||||
"highway=bridleway",
|
||||
"highway=living_street",
|
||||
"highway=track"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
},
|
||||
"wayHandling": 2,
|
||||
"minzoom": 12,
|
||||
"overpassTags": {
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"leisure=pitch"
|
||||
]
|
||||
]}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
@ -211,13 +212,13 @@
|
|||
"render": "./assets/layers/sport_pitch/tabletennis.svg"
|
||||
},
|
||||
"width": {
|
||||
"render": "8"
|
||||
"render": "1"
|
||||
},
|
||||
"iconSize": {
|
||||
"render": "40,40,center"
|
||||
},
|
||||
"color": {
|
||||
"render": "#00f"
|
||||
"render": "#009"
|
||||
},
|
||||
"presets": [
|
||||
{
|
||||
|
|
|
@ -5,17 +5,19 @@
|
|||
"nl": "Bewakingscamera's"
|
||||
},
|
||||
"minzoom": 12,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"man_made=surveillance",
|
||||
{
|
||||
"or": [
|
||||
"surveillance:type=camera",
|
||||
"surveillance:type=ALPR",
|
||||
"surveillance:type=ANPR"
|
||||
]
|
||||
}
|
||||
]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"man_made=surveillance",
|
||||
{
|
||||
"or": [
|
||||
"surveillance:type=camera",
|
||||
"surveillance:type=ALPR",
|
||||
"surveillance:type=ANPR"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
"de": "Toiletten",
|
||||
"fr": "Toilettes"
|
||||
},
|
||||
"overpassTags": "amenity=toilets",
|
||||
"source": {
|
||||
"osmTags": "amenity=toilets"
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Toilet",
|
||||
|
|
|
@ -5,8 +5,12 @@
|
|||
"en": "Tree"
|
||||
},
|
||||
"minzoom": 14,
|
||||
"overpassTags": {
|
||||
"and": ["natural=tree"]
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"natural=tree"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
|
@ -38,7 +42,9 @@
|
|||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": ["height~^[0-9.]+$"]
|
||||
"and": [
|
||||
"height~^[0-9.]+$"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Hoogte: {height} m",
|
||||
|
@ -55,7 +61,9 @@
|
|||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": ["leaf_type=broadleaved"]
|
||||
"and": [
|
||||
"leaf_type=broadleaved"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "<img src=\"./assets/themes/trees/broadleaved.svg\" style=\"width:1.5em;height:1.5em\" alt=\"\"/> Loofboom",
|
||||
|
@ -64,7 +72,9 @@
|
|||
},
|
||||
{
|
||||
"if": {
|
||||
"and": ["leaf_type=needleleaved"]
|
||||
"and": [
|
||||
"leaf_type=needleleaved"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "<img src=\"./assets/themes/trees/needleleaved.svg\" style=\"width:1.5em;height:1.5em\" alt=\"\"/> Naaldboom",
|
||||
|
@ -73,7 +83,9 @@
|
|||
},
|
||||
{
|
||||
"if": {
|
||||
"and": ["leaf_type=leafless"]
|
||||
"and": [
|
||||
"leaf_type=leafless"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "<img src=\"./assets/themes/trees/leafless.svg\" style=\"width:1.5em;height:1.5em\" alt=\"\"/> Permanent bladloos",
|
||||
|
@ -91,7 +103,9 @@
|
|||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": ["denotation=landmark"]
|
||||
"and": [
|
||||
"denotation=landmark"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "De boom valt op door zijn grootte of prominente locatie. Hij is nuttig voor navigatie.",
|
||||
|
@ -100,7 +114,9 @@
|
|||
},
|
||||
{
|
||||
"if": {
|
||||
"and": ["denotation=natural_monument"]
|
||||
"and": [
|
||||
"denotation=natural_monument"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "De boom is een natuurlijk monument, bijvoorbeeld doordat hij bijzonder oud of van een waardevolle soort is.",
|
||||
|
@ -109,7 +125,9 @@
|
|||
},
|
||||
{
|
||||
"if": {
|
||||
"and": ["denotation=agricultural"]
|
||||
"and": [
|
||||
"denotation=agricultural"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "De boom wordt voor landbouwdoeleinden gebruikt, bijvoorbeeld in een boomgaard.",
|
||||
|
@ -118,7 +136,9 @@
|
|||
},
|
||||
{
|
||||
"if": {
|
||||
"and": ["denotation=park"]
|
||||
"and": [
|
||||
"denotation=park"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "De boom staat in een park of dergelijke (begraafplaats, schoolterrein, …).",
|
||||
|
@ -127,7 +147,9 @@
|
|||
},
|
||||
{
|
||||
"if": {
|
||||
"and": ["denotation=garden"]
|
||||
"and": [
|
||||
"denotation=garden"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "De boom staat in de tuin bij een woning/flatgebouw.",
|
||||
|
@ -136,7 +158,9 @@
|
|||
},
|
||||
{
|
||||
"if": {
|
||||
"and": ["denotation=avenue"]
|
||||
"and": [
|
||||
"denotation=avenue"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Dit is een laanboom.",
|
||||
|
@ -145,7 +169,9 @@
|
|||
},
|
||||
{
|
||||
"if": {
|
||||
"and": ["denotation=urban"]
|
||||
"and": [
|
||||
"denotation=urban"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "De boom staat in een woonkern.",
|
||||
|
@ -154,7 +180,9 @@
|
|||
},
|
||||
{
|
||||
"if": {
|
||||
"and": ["denotation=none"]
|
||||
"and": [
|
||||
"denotation=none"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "De boom staat buiten een woonkern.",
|
||||
|
@ -171,7 +199,9 @@
|
|||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": ["leaf_cycle=deciduous"]
|
||||
"and": [
|
||||
"leaf_cycle=deciduous"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Bladverliezend: de boom is een periode van het jaar kaal.",
|
||||
|
@ -180,7 +210,9 @@
|
|||
},
|
||||
{
|
||||
"if": {
|
||||
"and": ["leaf_cycle=evergreen"]
|
||||
"and": [
|
||||
"leaf_cycle=evergreen"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Groenblijvend.",
|
||||
|
@ -189,7 +221,9 @@
|
|||
}
|
||||
],
|
||||
"condition": {
|
||||
"and": ["leaf_type!~^leafless$"]
|
||||
"and": [
|
||||
"leaf_type!~^leafless$"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -351,13 +385,17 @@
|
|||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": ["leaf_type=broadleaved"]
|
||||
"and": [
|
||||
"leaf_type=broadleaved"
|
||||
]
|
||||
},
|
||||
"then": "circle:#ffffff;./assets/themes/trees/broadleaved.svg"
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": ["leaf_type=needleleaved"]
|
||||
"and": [
|
||||
"leaf_type=needleleaved"
|
||||
]
|
||||
},
|
||||
"then": "circle:#ffffff;./assets/themes/trees/needleleaved.svg"
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@
|
|||
"nl": "Een mooi uitzicht - ideaal om een foto toe te voegen wanneer iets niet in een andere categorie past",
|
||||
"de": "Ein schöner Aussichtspunkt oder eine schöne Aussicht. Ideal zum Hinzufügen eines Bildes, wenn keine andere Kategorie passt"
|
||||
},
|
||||
"overpassTags": "tourism=viewpoint",
|
||||
"source": {
|
||||
"osmTags": "tourism=viewpoint"
|
||||
},
|
||||
"minzoom": 14,
|
||||
"icon": "./assets/layers/viewpoint/viewpoint.svg",
|
||||
"iconSize": "20,20,center",
|
||||
|
|
48
assets/layers/village_green/grass_in_parks.json
Normal file
48
assets/layers/village_green/grass_in_parks.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"id": "grass_in_parks",
|
||||
"name": {
|
||||
"nl": "Toegankelijke grasvelden in parken"
|
||||
},
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"landuse=grass",
|
||||
{
|
||||
"or": [
|
||||
"access=public",
|
||||
"access=yes"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"overpassScript": "way[\"leisure\"=\"park\"];node(w);is_in;area._[\"leisure\"=\"park\"];(way(area)[\"landuse\"=\"grass\"]; node(w); );"
|
||||
},
|
||||
"minzoom": 0,
|
||||
"title": {
|
||||
"render": {
|
||||
"nl": "Speelweide in een park"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "name~*",
|
||||
"then": {
|
||||
"nl": "{name}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": "./assets/themes/playgrounds/playground.svg",
|
||||
"iconSize": "40,40,bottom",
|
||||
"width": "1",
|
||||
"color": "#0f0",
|
||||
"wayHandling": 2,
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"render": "Op dit grasveld in het park mag je spelen, picnicken, zitten, ..."
|
||||
},
|
||||
{
|
||||
"render": "{reviews(name, landuse=grass )}"
|
||||
}
|
||||
]
|
||||
}
|
37
assets/layers/village_green/village_green.json
Normal file
37
assets/layers/village_green/village_green.json
Normal file
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"id": "village_green",
|
||||
"name": {
|
||||
"nl": "Speelweide"
|
||||
},
|
||||
"source": {
|
||||
"osmTags": "landuse=village_green"
|
||||
},
|
||||
"minzoom": 0,
|
||||
"title": {
|
||||
"render": {
|
||||
"nl": "Speelweide"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "name~*",
|
||||
"then": {
|
||||
"nl": "{name}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": "./assets/themes/playgrounds/playground.svg",
|
||||
"iconSize": "40,40,bottom",
|
||||
"width": "1",
|
||||
"color": "#0f0",
|
||||
"wayHandling": 2,
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"render": "Dit is een klein stukje openbaar groen waar je mag spelen, picnicken, zitten, ..."
|
||||
},
|
||||
{
|
||||
"render": "{reviews(name, landuse=village_green )}"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -27,7 +27,9 @@
|
|||
"play_forest",
|
||||
"playground",
|
||||
"sport_pitch",
|
||||
"slow_roads"
|
||||
"slow_roads",
|
||||
"grass_in_parks",
|
||||
"village_green"
|
||||
],
|
||||
"roamingRenderings": []
|
||||
}
|
Loading…
Reference in a new issue