Remove legacy: the minOverlapPercentage can now be built with a calculated tag and isShown
This commit is contained in:
parent
53e70b9a9c
commit
ad406b5550
14 changed files with 237 additions and 252 deletions
|
@ -45,7 +45,6 @@ export default class LayerConfig {
|
||||||
width: TagRenderingConfig;
|
width: TagRenderingConfig;
|
||||||
dashArray: TagRenderingConfig;
|
dashArray: TagRenderingConfig;
|
||||||
wayHandling: number;
|
wayHandling: number;
|
||||||
hideUnderlayingFeaturesMinPercentage?: number;
|
|
||||||
|
|
||||||
presets: {
|
presets: {
|
||||||
title: Translation,
|
title: Translation,
|
||||||
|
@ -98,8 +97,13 @@ export default class LayerConfig {
|
||||||
console.warn(`Unofficial theme ${this.id} with custom javascript! This is a security risk`)
|
console.warn(`Unofficial theme ${this.id} with custom javascript! This is a security risk`)
|
||||||
}
|
}
|
||||||
this.calculatedTags = [];
|
this.calculatedTags = [];
|
||||||
for (const key in json.calculatedTags) {
|
for (const kv of json.calculatedTags) {
|
||||||
this.calculatedTags.push([key, json.calculatedTags[key]])
|
|
||||||
|
const index = kv.indexOf("=")
|
||||||
|
const key = kv.substring(0, index);
|
||||||
|
const code = kv.substring(index + 1);
|
||||||
|
|
||||||
|
this.calculatedTags.push([key, code])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +112,6 @@ export default class LayerConfig {
|
||||||
this.minzoom = json.minzoom ?? 0;
|
this.minzoom = json.minzoom ?? 0;
|
||||||
this.maxzoom = json.maxzoom ?? 1000;
|
this.maxzoom = json.maxzoom ?? 1000;
|
||||||
this.wayHandling = json.wayHandling ?? 0;
|
this.wayHandling = json.wayHandling ?? 0;
|
||||||
this.hideUnderlayingFeaturesMinPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0;
|
|
||||||
this.presets = (json.presets ?? []).map((pr, i) =>
|
this.presets = (json.presets ?? []).map((pr, i) =>
|
||||||
({
|
({
|
||||||
title: Translations.T(pr.title, `${context}.presets[${i}].title`),
|
title: Translations.T(pr.title, `${context}.presets[${i}].title`),
|
||||||
|
@ -215,6 +218,9 @@ export default class LayerConfig {
|
||||||
this.dashArray = tr("dashArray", "");
|
this.dashArray = tr("dashArray", "");
|
||||||
|
|
||||||
|
|
||||||
|
if(json["showIf"] !== undefined){
|
||||||
|
throw "Invalid key on layerconfig "+this.id+": showIf. Did you mean 'isShown' instead?";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public CustomCodeSnippets(): string[] {
|
public CustomCodeSnippets(): string[] {
|
||||||
|
|
|
@ -43,9 +43,17 @@ export interface LayerConfigJson {
|
||||||
source: {osmTags: AndOrTagConfigJson | string} | {geoJsonSource: string} | {overpassScript: string}
|
source: {osmTags: AndOrTagConfigJson | string} | {geoJsonSource: string} | {overpassScript: string}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A dictionary of 'key': 'js-expression'. These js-expressions will be calculated for every feature, giving extra tags to work with in the rest of the pipieline
|
*
|
||||||
|
* A list of extra tags to calculate, specified as "keyToAssignTo=javascript-expression".
|
||||||
|
* There are a few extra functions available. Refer to <a>Docs/CalculatedTags.md</a> for more information
|
||||||
|
* The functions will be run in order, e.g.
|
||||||
|
* [
|
||||||
|
* "_max_overlap_m2=Math.max(...feat.overlapsWith("someOtherLayer").map(o => o.overlap))
|
||||||
|
* "_max_overlap_ratio=Number(feat._max_overlap_m2)/feat.area
|
||||||
|
* ]
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
calculatedTags? : any;
|
calculatedTags? : 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.
|
* If set, this layer will not query overpass; but it'll still match the tags above which are by chance returned by other layers.
|
||||||
|
@ -145,14 +153,6 @@ export interface LayerConfigJson {
|
||||||
*/
|
*/
|
||||||
wayHandling?: number;
|
wayHandling?: number;
|
||||||
|
|
||||||
/**
|
|
||||||
* Consider that we want to show 'Nature Reserves' and 'Forests'. Now, ofter, there are pieces of forest mapped _in_ the nature reserve.
|
|
||||||
* Now, showing those pieces of forest overlapping with the nature reserve truly clutters the map and is very user-unfriendly.
|
|
||||||
*
|
|
||||||
* The features are placed layer by layer. If a feature below a feature on this layer overlaps for more then 'x'-percent, the underlying feature is hidden.
|
|
||||||
*/
|
|
||||||
hideUnderlayingFeaturesMinPercentage?:number;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If set, this layer will pass all the features it receives onto the next layer.
|
* If set, this layer will pass all the features it receives onto the next layer.
|
||||||
* This is ideal for decoration, e.g. directionss on cameras
|
* This is ideal for decoration, e.g. directionss on cameras
|
||||||
|
|
|
@ -17,12 +17,12 @@ import {UIEventSource} from "../UIEventSource";
|
||||||
* Note that this list is embedded into an UIEVentSource, ready to put it into a carousel.
|
* Note that this list is embedded into an UIEVentSource, ready to put it into a carousel.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]>{
|
export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]> {
|
||||||
|
|
||||||
private readonly _wdItem = new UIEventSource<string>("");
|
private readonly _wdItem = new UIEventSource<string>("");
|
||||||
private readonly _commons = new UIEventSource<string>("");
|
private readonly _commons = new UIEventSource<string>("");
|
||||||
|
|
||||||
constructor(tags: UIEventSource<any>, imagePrefix = "image", loadSpecial = true) {
|
private constructor(tags: UIEventSource<any>, imagePrefix = "image", loadSpecial = true) {
|
||||||
super([])
|
super([])
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]>
|
||||||
let somethingChanged = false;
|
let somethingChanged = false;
|
||||||
for (const image of images) {
|
for (const image of images) {
|
||||||
const url = image.url;
|
const url = image.url;
|
||||||
const key = image.key;
|
|
||||||
|
|
||||||
if (url === undefined || url === null || url === "") {
|
if (url === undefined || url === null || url === "") {
|
||||||
continue;
|
continue;
|
||||||
|
@ -114,7 +113,6 @@ export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]>
|
||||||
imageURLS.push(wd.image);
|
imageURLS.push(wd.image);
|
||||||
Wikimedia.GetCategoryFiles(wd.commonsWiki, (images: ImagesInCategory) => {
|
Wikimedia.GetCategoryFiles(wd.commonsWiki, (images: ImagesInCategory) => {
|
||||||
for (const image of images.images) {
|
for (const image of images.images) {
|
||||||
// @ts-ignore
|
|
||||||
if (image.startsWith("File:")) {
|
if (image.startsWith("File:")) {
|
||||||
imageURLS.push(image);
|
imageURLS.push(image);
|
||||||
}
|
}
|
||||||
|
@ -129,17 +127,15 @@ export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]>
|
||||||
const imageUrls = [];
|
const imageUrls = [];
|
||||||
const allCommons: string[] = commonsData.split(";");
|
const allCommons: string[] = commonsData.split(";");
|
||||||
for (const commons of allCommons) {
|
for (const commons of allCommons) {
|
||||||
// @ts-ignore
|
|
||||||
if (commons.startsWith("Category:")) {
|
if (commons.startsWith("Category:")) {
|
||||||
Wikimedia.GetCategoryFiles(commons, (images: ImagesInCategory) => {
|
Wikimedia.GetCategoryFiles(commons, (images: ImagesInCategory) => {
|
||||||
for (const image of images.images) {
|
for (const image of images.images) {
|
||||||
// @ts-ignore
|
|
||||||
if (image.startsWith("File:")) {
|
if (image.startsWith("File:")) {
|
||||||
imageUrls.push(image);
|
imageUrls.push(image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else { // @ts-ignore
|
} else {
|
||||||
if (commons.startsWith("File:")) {
|
if (commons.startsWith("File:")) {
|
||||||
imageUrls.push(commons);
|
imageUrls.push(commons);
|
||||||
}
|
}
|
||||||
|
@ -169,4 +165,17 @@ export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]>
|
||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static _cache = new Map<string, ImageSearcher>();
|
||||||
|
|
||||||
|
public static construct(tags: UIEventSource<any>, imagePrefix = "image", loadSpecial = true): ImageSearcher {
|
||||||
|
const key = tags["id"] + " "+imagePrefix+loadSpecial;
|
||||||
|
if(ImageSearcher._cache.has(key)){
|
||||||
|
return ImageSearcher._cache.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const searcher = new ImageSearcher(tags, imagePrefix, loadSpecial);
|
||||||
|
ImageSearcher._cache.set(key, searcher)
|
||||||
|
return searcher;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -158,7 +158,7 @@ export default class UpdateFromOverpass implements FeatureSource {
|
||||||
self.retries.data++;
|
self.retries.data++;
|
||||||
self.ForceRefresh();
|
self.ForceRefresh();
|
||||||
self.timeout.setData(self.retries.data * 5);
|
self.timeout.setData(self.retries.data * 5);
|
||||||
console.log(`QUERY FAILED (retrying in ${5 * self.retries.data} sec)`, reason);
|
console.log(`QUERY FAILED (retrying in ${5 * self.retries.data} sec)`);
|
||||||
self.retries.ping();
|
self.retries.ping();
|
||||||
self.runningQuery.setData(false);
|
self.runningQuery.setData(false);
|
||||||
|
|
||||||
|
|
|
@ -5,59 +5,9 @@ import Combine from "../UI/Base/Combine";
|
||||||
export class ExtraFunction {
|
export class ExtraFunction {
|
||||||
|
|
||||||
|
|
||||||
private static DistanceToFunc = new ExtraFunction(
|
|
||||||
"distanceTo",
|
|
||||||
"Calculates the distance between the feature and a specified point",
|
|
||||||
["longitude", "latitude"],
|
|
||||||
(feature) => {
|
|
||||||
return (lon, lat) => {
|
|
||||||
// Feature._lon and ._lat is conveniently place by one of the other metatags
|
|
||||||
return GeoOperations.distanceBetween([lon, lat], [feature._lon, feature._lat]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
private static readonly allFuncs: ExtraFunction[] = [ExtraFunction.DistanceToFunc];
|
|
||||||
private readonly _name: string;
|
|
||||||
private readonly _args: string[];
|
|
||||||
private readonly _doc: string;
|
|
||||||
private readonly _f: (feat: any) => any;
|
|
||||||
|
|
||||||
constructor(name: string, doc: string, args: string[], f: ((feat: any) => any)) {
|
|
||||||
this._name = name;
|
|
||||||
this._doc = doc;
|
|
||||||
this._args = args;
|
|
||||||
this._f = f;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static FullPatchFeature(feature) {
|
|
||||||
for (const func of ExtraFunction.allFuncs) {
|
|
||||||
func.PatchFeature(feature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HelpText(): UIElement {
|
|
||||||
return new Combine([
|
|
||||||
ExtraFunction.intro,
|
|
||||||
...ExtraFunction.allFuncs.map(func =>
|
|
||||||
new Combine([
|
|
||||||
"<h3>" + func._name + "</h3>",
|
|
||||||
func._doc,
|
|
||||||
"<ul>",
|
|
||||||
...func._args.map(arg => "<li>" + arg + "</li>"),
|
|
||||||
"</ul>"
|
|
||||||
])
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PatchFeature(feature: any) {
|
|
||||||
feature[this._name] = this._f(feature);
|
|
||||||
}
|
|
||||||
|
|
||||||
static readonly intro = `<h2>Calculating tags with Javascript</h2>
|
static readonly intro = `<h2>Calculating tags with Javascript</h2>
|
||||||
|
|
||||||
<p>In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. <b>_lat</b>, <b>lon</b>, <b>_country</b>), as detailed above.</p>
|
<p>In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. <b>lat</b>, <b>lon</b>, <b>_country</b>), as detailed above.</p>
|
||||||
|
|
||||||
<p>It is also possible to calculate your own tags - but this requires some javascript knowledge. </p>
|
<p>It is also possible to calculate your own tags - but this requires some javascript knowledge. </p>
|
||||||
|
|
||||||
|
@ -71,11 +21,97 @@ Before proceeding, some warnings:
|
||||||
In the layer object, add a field <b>calculatedTags</b>, e.g.:
|
In the layer object, add a field <b>calculatedTags</b>, e.g.:
|
||||||
|
|
||||||
<div class="code">
|
<div class="code">
|
||||||
"calculatedTags": {
|
"calculatedTags": [
|
||||||
"_someKey": "javascript-expression",
|
"_someKey=javascript-expression",
|
||||||
"name": "feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",
|
"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",
|
||||||
"_distanceCloserThen3Km": "feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'"
|
"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'"
|
||||||
}
|
]
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
The above code will be executed for every feature in the layer. The feature is accessible as <b>feat</b> and is an amended geojson object:
|
||||||
|
- <b>area</b> contains the surface area (in square meters) of the object
|
||||||
|
- <b>lat</b> and <b>lon</b> contain the latitude and longitude
|
||||||
|
|
||||||
|
Some advanced functions are available on <b>feat</b> as well:
|
||||||
|
|
||||||
`
|
`
|
||||||
|
private static OverlapFunc = new ExtraFunction(
|
||||||
|
"overlapWith",
|
||||||
|
"Gives a list of features from the specified layer which this feature overlaps with, the amount of overlap in m². The returned value is <b>{ feat: GeoJSONFeature, overlap: number}</b>",
|
||||||
|
["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"],
|
||||||
|
(featuresPerLayer, feat) => {
|
||||||
|
return (...layerIds: string[]) => {
|
||||||
|
const result = []
|
||||||
|
for (const layerId of layerIds) {
|
||||||
|
const otherLayer = featuresPerLayer.get(layerId);
|
||||||
|
if (otherLayer === undefined) {
|
||||||
|
console.error(`Trying to calculate 'overlapWith' with specified layer ${layerId}, but such layer is found`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherLayer.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
private static DistanceToFunc = new ExtraFunction(
|
||||||
|
"distanceTo",
|
||||||
|
"Calculates the distance between the feature and a specified point",
|
||||||
|
["longitude", "latitude"],
|
||||||
|
(featuresPerLayer, feature) => {
|
||||||
|
return (lon, lat) => {
|
||||||
|
// Feature._lon and ._lat is conveniently place by one of the other metatags
|
||||||
|
return GeoOperations.distanceBetween([lon, lat], [feature._lon, feature._lat]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
private static readonly allFuncs: ExtraFunction[] = [ExtraFunction.DistanceToFunc, ExtraFunction.OverlapFunc];
|
||||||
|
private readonly _name: string;
|
||||||
|
private readonly _args: string[];
|
||||||
|
private readonly _doc: string;
|
||||||
|
private readonly _f: (featuresPerLayer: Map<string, any[]>, feat: any) => any;
|
||||||
|
|
||||||
|
constructor(name: string, doc: string, args: string[], f: ((featuresPerLayer: Map<string, any[]>, feat: any) => any)) {
|
||||||
|
this._name = name;
|
||||||
|
this._doc = doc;
|
||||||
|
this._args = args;
|
||||||
|
this._f = f;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FullPatchFeature(featuresPerLayer: Map<string, any[]>, feature) {
|
||||||
|
for (const func of ExtraFunction.allFuncs) {
|
||||||
|
func.PatchFeature(featuresPerLayer, feature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HelpText(): UIElement {
|
||||||
|
return new Combine([
|
||||||
|
ExtraFunction.intro,
|
||||||
|
"<ul>",
|
||||||
|
...ExtraFunction.allFuncs.map(func =>
|
||||||
|
new Combine([
|
||||||
|
"<li>", func._name, "</li>"
|
||||||
|
])
|
||||||
|
),
|
||||||
|
"</ul>",
|
||||||
|
...ExtraFunction.allFuncs.map(func =>
|
||||||
|
new Combine([
|
||||||
|
"<h3>" + func._name + "</h3>",
|
||||||
|
func._doc,
|
||||||
|
"<ul>",
|
||||||
|
...func._args.map(arg => "<li>" + arg + "</li>"),
|
||||||
|
"</ul>"
|
||||||
|
])
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PatchFeature(featuresPerLayer: Map<string, any[]>, feature: any) {
|
||||||
|
feature[this._name] = this._f(featuresPerLayer, feature);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,7 +2,6 @@ import FilteringFeatureSource from "../FeatureSource/FilteringFeatureSource";
|
||||||
import FeatureSourceMerger from "../FeatureSource/FeatureSourceMerger";
|
import FeatureSourceMerger from "../FeatureSource/FeatureSourceMerger";
|
||||||
import RememberingSource from "../FeatureSource/RememberingSource";
|
import RememberingSource from "../FeatureSource/RememberingSource";
|
||||||
import WayHandlingApplyingFeatureSource from "../FeatureSource/WayHandlingApplyingFeatureSource";
|
import WayHandlingApplyingFeatureSource from "../FeatureSource/WayHandlingApplyingFeatureSource";
|
||||||
import NoOverlapSource from "../FeatureSource/NoOverlapSource";
|
|
||||||
import FeatureDuplicatorPerLayer from "../FeatureSource/FeatureDuplicatorPerLayer";
|
import FeatureDuplicatorPerLayer from "../FeatureSource/FeatureDuplicatorPerLayer";
|
||||||
import FeatureSource from "../FeatureSource/FeatureSource";
|
import FeatureSource from "../FeatureSource/FeatureSource";
|
||||||
import {UIEventSource} from "../UIEventSource";
|
import {UIEventSource} from "../UIEventSource";
|
||||||
|
@ -25,9 +24,8 @@ export default class FeaturePipeline implements FeatureSource {
|
||||||
locationControl: UIEventSource<Loc>) {
|
locationControl: UIEventSource<Loc>) {
|
||||||
|
|
||||||
const amendedOverpassSource =
|
const amendedOverpassSource =
|
||||||
new RememberingSource(
|
new RememberingSource(new FeatureDuplicatorPerLayer(flayers,
|
||||||
new NoOverlapSource(flayers, new FeatureDuplicatorPerLayer(flayers,
|
new LocalStorageSaver(updater, layout))
|
||||||
new LocalStorageSaver(updater, layout)))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const geojsonSources: GeoJsonSource [] = []
|
const geojsonSources: GeoJsonSource [] = []
|
||||||
|
@ -40,8 +38,7 @@ export default class FeaturePipeline implements FeatureSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
const amendedLocalStorageSource =
|
const amendedLocalStorageSource =
|
||||||
new RememberingSource(
|
new RememberingSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout))
|
||||||
new NoOverlapSource(flayers, new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout)))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
newPoints = new FeatureDuplicatorPerLayer(flayers, newPoints);
|
newPoints = new FeatureDuplicatorPerLayer(flayers, newPoints);
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
|
||||||
import FeatureSource from "./FeatureSource";
|
|
||||||
import {UIEventSource} from "../UIEventSource";
|
|
||||||
import {GeoOperations} from "../GeoOperations";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The no overlap source takes a featureSource and applies a filter on it.
|
|
||||||
* First, it'll figure out for each feature to which layer it belongs
|
|
||||||
* Then, it'll check any feature of any 'lower' layer
|
|
||||||
*/
|
|
||||||
export default class NoOverlapSource {
|
|
||||||
|
|
||||||
features: UIEventSource<{ feature: any, freshness: Date }[]> = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
|
|
||||||
|
|
||||||
constructor(layers: UIEventSource<{
|
|
||||||
layerDef: LayerConfig
|
|
||||||
}[]>,
|
|
||||||
upstream: FeatureSource) {
|
|
||||||
let noOverlapRemoval = true;
|
|
||||||
for (const layer of layers.data) {
|
|
||||||
if ((layer.layerDef.hideUnderlayingFeaturesMinPercentage ?? 0) !== 0) {
|
|
||||||
noOverlapRemoval = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (noOverlapRemoval) {
|
|
||||||
this.features = upstream.features;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.features = upstream.features.map(
|
|
||||||
features => {
|
|
||||||
if (features === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const layerIds = []
|
|
||||||
const layerDict = {};
|
|
||||||
for (const layer of layers.data) {
|
|
||||||
layerDict[layer.layerDef.id] = layer;
|
|
||||||
layerIds.push(layer.layerDef.id);
|
|
||||||
if ((layer.layerDef.hideUnderlayingFeaturesMinPercentage ?? 0) !== 0) {
|
|
||||||
noOverlapRemoval = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// There is overlap removal active
|
|
||||||
// We partition all the features with their respective layerIDs
|
|
||||||
const partitions = {};
|
|
||||||
for (const layerId of layerIds) {
|
|
||||||
partitions[layerId] = []
|
|
||||||
}
|
|
||||||
for (const feature of features) {
|
|
||||||
partitions[feature.feature._matching_layer_id].push(feature);
|
|
||||||
}
|
|
||||||
|
|
||||||
// With this partitioning in hand, we run over every layer and remove every underlying feature if needed
|
|
||||||
for (let i = 0; i < layerIds.length; i++) {
|
|
||||||
let layerId = layerIds[i];
|
|
||||||
const percentage = layerDict[layerId].layerDef.hideUnderlayingFeaturesMinPercentage ?? 0;
|
|
||||||
if (percentage === 0) {
|
|
||||||
// We don't have to remove underlying features!
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const guardPartition = partitions[layerId];
|
|
||||||
for (let j = i + 1; j < layerIds.length; j++) {
|
|
||||||
let layerJd = layerIds[j];
|
|
||||||
let partitionToShrink: { feature: any, freshness: Date }[] = partitions[layerJd];
|
|
||||||
let newPartition = [];
|
|
||||||
for (const mightBeDeleted of partitionToShrink) {
|
|
||||||
const doesOverlap = GeoOperations.featureIsContainedInAny(
|
|
||||||
mightBeDeleted.feature,
|
|
||||||
guardPartition.map(f => f.feature),
|
|
||||||
percentage
|
|
||||||
);
|
|
||||||
if (!doesOverlap) {
|
|
||||||
newPartition.push(mightBeDeleted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
partitions[layerJd] = newPartition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// At last, we create the actual new features
|
|
||||||
let newFeatures: { feature: any, freshness: Date }[] = [];
|
|
||||||
for (const layerId of layerIds) {
|
|
||||||
newFeatures = newFeatures.concat(partitions[layerId]);
|
|
||||||
}
|
|
||||||
return newFeatures;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,67 +30,61 @@ export class GeoOperations {
|
||||||
return turf.distance(lonlat0, lonlat1)
|
return turf.distance(lonlat0, lonlat1)
|
||||||
}
|
}
|
||||||
|
|
||||||
static featureIsContainedInAny(feature: any,
|
/**
|
||||||
shouldNotContain: any[],
|
* Calculates the overlap of 'feature' with every other specified feature.
|
||||||
maxOverlapPercentage: number): boolean {
|
* The features with which 'feature' overlaps, are returned together with their overlap area in m²
|
||||||
// Returns 'false' if no problematic intersection is found
|
*
|
||||||
|
* If 'feature' is a point, it will return every feature the point is embedded in. Overlap will be undefined
|
||||||
|
*/
|
||||||
|
static calculateOverlap(feature: any,
|
||||||
|
otherFeatures: any[]): { feat: any, overlap: number }[] {
|
||||||
|
const featureBBox = BBox.get(feature);
|
||||||
|
const result : { feat: any, overlap: number }[] = [];
|
||||||
if (feature.geometry.type === "Point") {
|
if (feature.geometry.type === "Point") {
|
||||||
const coor = feature.geometry.coordinates;
|
const coor = feature.geometry.coordinates;
|
||||||
for (const shouldNotContainElement of shouldNotContain) {
|
for (const otherFeature of otherFeatures) {
|
||||||
|
|
||||||
let shouldNotContainBBox = BBox.get(shouldNotContainElement);
|
let otherFeatureBBox = BBox.get(otherFeature);
|
||||||
let featureBBox = BBox.get(feature);
|
if (!featureBBox.overlapsWith(otherFeatureBBox)) {
|
||||||
if (!featureBBox.overlapsWith(shouldNotContainBBox)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.inside(coor, shouldNotContainElement)) {
|
if (this.inside(coor, otherFeatures)) {
|
||||||
return true
|
result.push({ feat: otherFeatures, overlap: undefined })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") {
|
if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") {
|
||||||
|
|
||||||
const poly = feature;
|
for (const otherFeature of otherFeatures) {
|
||||||
let featureBBox = BBox.get(feature);
|
const otherFeatureBBox = BBox.get(otherFeature);
|
||||||
const featureSurface = GeoOperations.surfaceAreaInSqMeters(poly);
|
const overlaps = featureBBox.overlapsWith(otherFeatureBBox)
|
||||||
for (const shouldNotContainElement of shouldNotContain) {
|
|
||||||
|
|
||||||
const shouldNotContainBBox = BBox.get(shouldNotContainElement);
|
|
||||||
const overlaps = featureBBox.overlapsWith(shouldNotContainBBox)
|
|
||||||
if (!overlaps) {
|
if (!overlaps) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the surface area of the intersection
|
// Calculate the surface area of the intersection
|
||||||
// If it is too big, refuse
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const intersection = turf.intersect(poly, shouldNotContainElement);
|
const intersection = turf.intersect(feature, otherFeature);
|
||||||
if (intersection == null) {
|
if (intersection == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const intersectionSize = turf.area(intersection);
|
const intersectionSize = turf.area(intersection); // in m²
|
||||||
const ratio = intersectionSize / featureSurface;
|
result.push({feat: otherFeature, overlap: intersectionSize})
|
||||||
|
|
||||||
if (ratio * 100 >= maxOverlapPercentage) {
|
|
||||||
console.log("Refused", poly.id, " due to ", shouldNotContainElement.id, "intersection ratio is ", ratio, "which is bigger then the target ratio of ", (maxOverlapPercentage / 100))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
console.log("EXCEPTION CAUGHT WHILE INTERSECTING: ", exception);
|
console.log("EXCEPTION CAUGHT WHILE INTERSECTING: ", exception);
|
||||||
// We assume that this failed due to an intersection
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return false; // No problematic intersections found
|
return result;
|
||||||
}
|
}
|
||||||
|
console.error("Could not correctly calculate the overlap of ", feature, ": unsupported type")
|
||||||
return false;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static inside(pointCoordinate, feature): boolean {
|
public static inside(pointCoordinate, feature): boolean {
|
||||||
// ray-casting algorithm based on
|
// ray-casting algorithm based on
|
||||||
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
|
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
|
||||||
|
|
|
@ -26,11 +26,21 @@ export default class MetaTagging {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The functions - per layer - which add the new keys
|
// The functions - per layer - which add the new keys
|
||||||
const layerFuncs = new Map<string, ((feature: any) => void)>();
|
const layerFuncs = new Map<string, ((featursPerLayer: Map<string, any[]>, feature: any) => void)>();
|
||||||
for (const layer of layers) {
|
for (const layer of layers) {
|
||||||
layerFuncs.set(layer.id, this.createRetaggingFunc(layer));
|
layerFuncs.set(layer.id, this.createRetaggingFunc(layer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const featuresPerLayer = new Map<string, any[]>();
|
||||||
|
for (const feature of features) {
|
||||||
|
|
||||||
|
const key = feature.feature._matching_layer_id;
|
||||||
|
if (!featuresPerLayer.has(key)) {
|
||||||
|
featuresPerLayer.set(key, [])
|
||||||
|
}
|
||||||
|
featuresPerLayer.get(key).push(feature.feature)
|
||||||
|
}
|
||||||
|
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const key = feature.feature._matching_layer_id;
|
const key = feature.feature._matching_layer_id;
|
||||||
|
@ -39,19 +49,19 @@ export default class MetaTagging {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
f(feature.feature)
|
f(featuresPerLayer, feature.feature)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static createRetaggingFunc(layer: LayerConfig): ((feature: any) => void) {
|
private static createRetaggingFunc(layer: LayerConfig): ((featuresPerLayer: Map<string, any[]>, feature: any) => void) {
|
||||||
const calculatedTags: [string, string][] = layer.calculatedTags;
|
const calculatedTags: [string, string][] = layer.calculatedTags;
|
||||||
if (calculatedTags === undefined) {
|
if (calculatedTags === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const functions: ((feature: any) => void)[] = [];
|
const functions: ((featuresPerLayer: Map<string, any[]>, feature: any) => void)[] = [];
|
||||||
for (const entry of calculatedTags) {
|
for (const entry of calculatedTags) {
|
||||||
const key = entry[0]
|
const key = entry[0]
|
||||||
const code = entry[1];
|
const code = entry[1];
|
||||||
|
@ -61,26 +71,24 @@ export default class MetaTagging {
|
||||||
|
|
||||||
const func = new Function("feat", "return " + code + ";");
|
const func = new Function("feat", "return " + code + ";");
|
||||||
|
|
||||||
const f = (feature: any) => {
|
const f = (featuresPerLayer, feature: any) => {
|
||||||
feature.properties[key] = func(feature);
|
feature.properties[key] = func(feature);
|
||||||
}
|
}
|
||||||
functions.push(f)
|
functions.push(f)
|
||||||
}
|
}
|
||||||
return (feature) => {
|
return (featuresPerLayer: Map<string, any[]>, feature) => {
|
||||||
const tags = feature.properties
|
const tags = feature.properties
|
||||||
if (tags === undefined) {
|
if (tags === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtraFunction.FullPatchFeature(feature);
|
ExtraFunction.FullPatchFeature(featuresPerLayer, feature);
|
||||||
|
try {
|
||||||
for (const f of functions) {
|
for (const f of functions) {
|
||||||
try {
|
f(featuresPerLayer, feature);
|
||||||
f(feature);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("While calculating a tag value: ", e)
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("While calculating a tag value: ", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ export default class SimpleMetaTagger {
|
||||||
const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature);
|
const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature);
|
||||||
feature.properties["_surface"] = "" + sqMeters;
|
feature.properties["_surface"] = "" + sqMeters;
|
||||||
feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10;
|
feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10;
|
||||||
|
feature.area = sqMeters;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
private static country = new SimpleMetaTagger(
|
private static country = new SimpleMetaTagger(
|
||||||
|
|
|
@ -5,13 +5,13 @@ import * as $ from "jquery"
|
||||||
*/
|
*/
|
||||||
export class Wikimedia {
|
export class Wikimedia {
|
||||||
|
|
||||||
|
private static knownLicenses = {};
|
||||||
|
|
||||||
static ImageNameToUrl(filename: string, width: number = 500, height: number = 200): string {
|
static ImageNameToUrl(filename: string, width: number = 500, height: number = 200): string {
|
||||||
filename = encodeURIComponent(filename);
|
filename = encodeURIComponent(filename);
|
||||||
return "https://commons.wikimedia.org/wiki/Special:FilePath/" + filename + "?width=" + width + "&height=" + height;
|
return "https://commons.wikimedia.org/wiki/Special:FilePath/" + filename + "?width=" + width + "&height=" + height;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static knownLicenses = {};
|
|
||||||
|
|
||||||
static LicenseData(filename: string, handle: ((LicenseInfo) => void)): void {
|
static LicenseData(filename: string, handle: ((LicenseInfo) => void)): void {
|
||||||
if (filename in this.knownLicenses) {
|
if (filename in this.knownLicenses) {
|
||||||
return this.knownLicenses[filename];
|
return this.knownLicenses[filename];
|
||||||
|
@ -42,8 +42,9 @@ export class Wikimedia {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static GetCategoryFiles(categoryName: string, handleCategory: ((ImagesInCategory) => void),
|
static GetCategoryFiles(categoryName: string, handleCategory: ((ImagesInCategory: ImagesInCategory) => void),
|
||||||
alreadyLoaded = 0, continueParameter: { k: string, param: string } = undefined) {
|
alreadyLoaded = 0,
|
||||||
|
continueParameter: { k: string, param: string } = undefined) {
|
||||||
if (categoryName === undefined || categoryName === null || categoryName === "") {
|
if (categoryName === undefined || categoryName === null || categoryName === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -58,7 +59,8 @@ export class Wikimedia {
|
||||||
if (continueParameter !== undefined) {
|
if (continueParameter !== undefined) {
|
||||||
url = url + "&" + continueParameter.k + "=" + continueParameter.param;
|
url = url + "&" + continueParameter.k + "=" + continueParameter.param;
|
||||||
}
|
}
|
||||||
|
const self = this;
|
||||||
|
console.log("Loading a wikimedia category: ", url)
|
||||||
$.getJSON(url, (response) => {
|
$.getJSON(url, (response) => {
|
||||||
let imageOverview = new ImagesInCategory();
|
let imageOverview = new ImagesInCategory();
|
||||||
let members = response.query?.categorymembers;
|
let members = response.query?.categorymembers;
|
||||||
|
@ -67,21 +69,27 @@ export class Wikimedia {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const member of members) {
|
for (const member of members) {
|
||||||
|
|
||||||
imageOverview.images.push(member.title);
|
imageOverview.images.push(member.title);
|
||||||
}
|
}
|
||||||
if (response.continue === undefined || alreadyLoaded > 30) {
|
console.log("Got images! ", imageOverview)
|
||||||
|
if (response.continue === undefined) {
|
||||||
handleCategory(imageOverview);
|
handleCategory(imageOverview);
|
||||||
} else {
|
return;
|
||||||
console.log("Recursive load for ", categoryName)
|
}
|
||||||
this.GetCategoryFiles(categoryName, (recursiveImages) => {
|
|
||||||
for (const image of imageOverview.images) {
|
if (alreadyLoaded > 10) {
|
||||||
recursiveImages.images.push(image);
|
console.log(`Recursive wikimedia category load stopped for ${categoryName} - got already enough images now (${alreadyLoaded})`)
|
||||||
}
|
handleCategory(imageOverview)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.GetCategoryFiles(categoryName,
|
||||||
|
(recursiveImages) => {
|
||||||
|
recursiveImages.images.push(...imageOverview.images);
|
||||||
handleCategory(recursiveImages);
|
handleCategory(recursiveImages);
|
||||||
},
|
},
|
||||||
alreadyLoaded + 10, {k: "cmcontinue", param: response.continue.cmcontinue})
|
alreadyLoaded + 10,
|
||||||
}
|
{k: "cmcontinue", param: response.continue.cmcontinue})
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -104,7 +112,6 @@ export class Wikimedia {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Wikidata {
|
export class Wikidata {
|
||||||
|
|
|
@ -98,10 +98,6 @@ export default class LayerPanel extends UIElement {
|
||||||
{value: 2, shown: "Show both the ways/areas and the centerpoints"},
|
{value: 2, shown: "Show both the ways/areas and the centerpoints"},
|
||||||
{value: 1, shown: "Show everything as centerpoint"}]), "wayHandling", "Way handling",
|
{value: 1, shown: "Show everything as centerpoint"}]), "wayHandling", "Way handling",
|
||||||
"Describes how ways and areas are represented on the map: areas can be represented as the area itself, or it can be converted into the centerpoint"),
|
"Describes how ways and areas are represented on the map: areas can be represented as the area itself, or it can be converted into the centerpoint"),
|
||||||
setting(ValidatedTextField.NumberInput("int", n => n <= 100), "hideUnderlayingFeaturesMinPercentage", "Max allowed overlap percentage",
|
|
||||||
"Consider that we want to show 'Nature Reserves' and 'Forests'. Now, ofter, there are pieces of forest mapped _in_ the nature reserve.<br/>" +
|
|
||||||
"Now, showing those pieces of forest overlapping with the nature reserve truly clutters the map and is very user-unfriendly.<br/>" +
|
|
||||||
"The features are placed layer by layer. If a feature below a feature on this layer overlaps for more then 'x'-percent, the underlying feature is hidden."),
|
|
||||||
setting(new AndOrTagInput(), ["osmSource","overpassTags"], "Overpass query",
|
setting(new AndOrTagInput(), ["osmSource","overpassTags"], "Overpass query",
|
||||||
"The tags of the objects to load from overpass"),
|
"The tags of the objects to load from overpass"),
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default class SpecialVisualizations {
|
||||||
constr: (state: State, tags, args) => {
|
constr: (state: State, tags, args) => {
|
||||||
const imagePrefix = args[0];
|
const imagePrefix = args[0];
|
||||||
const loadSpecial = args[1].toLowerCase() === "true";
|
const loadSpecial = args[1].toLowerCase() === "true";
|
||||||
const searcher: UIEventSource<{ key: string, url: string }[]> = new ImageSearcher(tags, imagePrefix, loadSpecial);
|
const searcher: UIEventSource<{ key: string, url: string }[]> = ImageSearcher.construct(tags, imagePrefix, loadSpecial);
|
||||||
|
|
||||||
return new ImageCarousel(searcher, tags);
|
return new ImageCarousel(searcher, tags);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
"startLat": 50.8435,
|
"startLat": 50.8435,
|
||||||
"startLon": 4.3688,
|
"startLon": 4.3688,
|
||||||
"startZoom": 16,
|
"startZoom": 16,
|
||||||
"widenFactor": 0.05,
|
"widenFactor": 0.01,
|
||||||
"socialImage": "./assets/themes/buurtnatuur/social_image.jpg",
|
"socialImage": "./assets/themes/buurtnatuur/social_image.jpg",
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
|
@ -75,7 +75,6 @@
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
"images"
|
"images"
|
||||||
],
|
],
|
||||||
"hideUnderlayingFeaturesMinPercentage": 10,
|
|
||||||
"icon": {
|
"icon": {
|
||||||
"render": "circle:#ffffff;./assets/themes/buurtnatuur/nature_reserve.svg"
|
"render": "circle:#ffffff;./assets/themes/buurtnatuur/nature_reserve.svg"
|
||||||
},
|
},
|
||||||
|
@ -141,6 +140,19 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"calculatedTags": [
|
||||||
|
"_overlapWithUpperLayers=Math.max(...feat.overlapWith('nature_reserve').map(o => o.overlap))/feat.area",
|
||||||
|
"_tooMuchOverlap=Number(feat.properties._overlapWithUpperLayers) > 0.1 ? 'yes' :'no'"
|
||||||
|
],
|
||||||
|
"isShown": {
|
||||||
|
"render": "yes",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "_tooMuchOverlap=yes",
|
||||||
|
"then": "no"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"render": {
|
"render": {
|
||||||
"nl": "Park"
|
"nl": "Park"
|
||||||
|
@ -149,7 +161,7 @@
|
||||||
{
|
{
|
||||||
"if": {
|
"if": {
|
||||||
"and": [
|
"and": [
|
||||||
"name:nl~"
|
"name:nl~*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"then": {
|
"then": {
|
||||||
|
@ -174,7 +186,6 @@
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
"images"
|
"images"
|
||||||
],
|
],
|
||||||
"hideUnderlayingFeaturesMinPercentage": 10,
|
|
||||||
"icon": {
|
"icon": {
|
||||||
"render": "circle:#ffffff;./assets/themes/buurtnatuur/park.svg"
|
"render": "circle:#ffffff;./assets/themes/buurtnatuur/park.svg"
|
||||||
},
|
},
|
||||||
|
@ -228,6 +239,19 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"calculatedTags": [
|
||||||
|
"_overlapWithUpperLayers=Math.max(...feat.overlapWith('parks','nature_reserve').map(o => o.overlap))/feat.area",
|
||||||
|
"_tooMuchOverlap=Number(feat.properties._overlapWithUpperLayers) > 0.1 ? 'yes' : 'no'"
|
||||||
|
],
|
||||||
|
"isShown": {
|
||||||
|
"render": "yes",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "_tooMuchOverlap=yes",
|
||||||
|
"then": "no"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"render": {
|
"render": {
|
||||||
"nl": "Bos"
|
"nl": "Bos"
|
||||||
|
@ -236,7 +260,7 @@
|
||||||
{
|
{
|
||||||
"if": {
|
"if": {
|
||||||
"and": [
|
"and": [
|
||||||
"name:nl~"
|
"name:nl~*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"then": {
|
"then": {
|
||||||
|
@ -261,7 +285,6 @@
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
"images"
|
"images"
|
||||||
],
|
],
|
||||||
"hideUnderlayingFeaturesMinPercentage": 0,
|
|
||||||
"icon": {
|
"icon": {
|
||||||
"render": "circle:#ffffff;./assets/themes/buurtnatuur/forest.svg"
|
"render": "circle:#ffffff;./assets/themes/buurtnatuur/forest.svg"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue