Remove legacy: the minOverlapPercentage can now be built with a calculated tag and isShown

This commit is contained in:
pietervdvn 2021-03-26 03:24:58 +01:00
parent 53e70b9a9c
commit ad406b5550
14 changed files with 237 additions and 252 deletions

View file

@ -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[] {

View file

@ -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

View file

@ -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;
}
} }

View file

@ -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);

View file

@ -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);
}
} }

View file

@ -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);

View file

@ -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;
});
}
}

View file

@ -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

View file

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

View file

@ -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(

View file

@ -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 {

View file

@ -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"),

View file

@ -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);
} }

View file

@ -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"
}, },