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
|
@ -24,7 +24,7 @@ export default class LayerConfig {
|
|||
static WAYHANDLING_DEFAULT = 0;
|
||||
static WAYHANDLING_CENTER_ONLY = 1;
|
||||
static WAYHANDLING_CENTER_AND_WAY = 2;
|
||||
|
||||
|
||||
id: string;
|
||||
name: Translation
|
||||
description: Translation;
|
||||
|
@ -45,7 +45,6 @@ export default class LayerConfig {
|
|||
width: TagRenderingConfig;
|
||||
dashArray: TagRenderingConfig;
|
||||
wayHandling: number;
|
||||
hideUnderlayingFeaturesMinPercentage?: number;
|
||||
|
||||
presets: {
|
||||
title: Translation,
|
||||
|
@ -98,8 +97,13 @@ export default class LayerConfig {
|
|||
console.warn(`Unofficial theme ${this.id} with custom javascript! This is a security risk`)
|
||||
}
|
||||
this.calculatedTags = [];
|
||||
for (const key in json.calculatedTags) {
|
||||
this.calculatedTags.push([key, json.calculatedTags[key]])
|
||||
for (const kv of json.calculatedTags) {
|
||||
|
||||
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.maxzoom = json.maxzoom ?? 1000;
|
||||
this.wayHandling = json.wayHandling ?? 0;
|
||||
this.hideUnderlayingFeaturesMinPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0;
|
||||
this.presets = (json.presets ?? []).map((pr, i) =>
|
||||
({
|
||||
title: Translations.T(pr.title, `${context}.presets[${i}].title`),
|
||||
|
@ -215,6 +218,9 @@ export default class LayerConfig {
|
|||
this.dashArray = tr("dashArray", "");
|
||||
|
||||
|
||||
if(json["showIf"] !== undefined){
|
||||
throw "Invalid key on layerconfig "+this.id+": showIf. Did you mean 'isShown' instead?";
|
||||
}
|
||||
}
|
||||
|
||||
public CustomCodeSnippets(): string[] {
|
||||
|
|
|
@ -43,9 +43,17 @@ export interface LayerConfigJson {
|
|||
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.
|
||||
|
@ -145,14 +153,6 @@ export interface LayerConfigJson {
|
|||
*/
|
||||
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.
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
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 _commons = new UIEventSource<string>("");
|
||||
|
||||
constructor(tags: UIEventSource<any>, imagePrefix = "image", loadSpecial = true) {
|
||||
private constructor(tags: UIEventSource<any>, imagePrefix = "image", loadSpecial = true) {
|
||||
super([])
|
||||
const self = this;
|
||||
|
||||
|
@ -31,7 +31,6 @@ export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]>
|
|||
let somethingChanged = false;
|
||||
for (const image of images) {
|
||||
const url = image.url;
|
||||
const key = image.key;
|
||||
|
||||
if (url === undefined || url === null || url === "") {
|
||||
continue;
|
||||
|
@ -93,7 +92,7 @@ export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]>
|
|||
if (mapillary.indexOf(prefix) < 0) {
|
||||
mapillary = prefix + mapillary;
|
||||
}
|
||||
|
||||
|
||||
|
||||
AddImages([{url: mapillary, key: undefined}]);
|
||||
}
|
||||
|
@ -114,7 +113,6 @@ export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]>
|
|||
imageURLS.push(wd.image);
|
||||
Wikimedia.GetCategoryFiles(wd.commonsWiki, (images: ImagesInCategory) => {
|
||||
for (const image of images.images) {
|
||||
// @ts-ignore
|
||||
if (image.startsWith("File:")) {
|
||||
imageURLS.push(image);
|
||||
}
|
||||
|
@ -129,17 +127,15 @@ export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]>
|
|||
const imageUrls = [];
|
||||
const allCommons: string[] = commonsData.split(";");
|
||||
for (const commons of allCommons) {
|
||||
// @ts-ignore
|
||||
if (commons.startsWith("Category:")) {
|
||||
Wikimedia.GetCategoryFiles(commons, (images: ImagesInCategory) => {
|
||||
for (const image of images.images) {
|
||||
// @ts-ignore
|
||||
if (image.startsWith("File:")) {
|
||||
imageUrls.push(image);
|
||||
}
|
||||
}
|
||||
})
|
||||
} else { // @ts-ignore
|
||||
} else {
|
||||
if (commons.startsWith("File:")) {
|
||||
imageUrls.push(commons);
|
||||
}
|
||||
|
@ -168,5 +164,18 @@ export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]>
|
|||
|
||||
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.ForceRefresh();
|
||||
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.runningQuery.setData(false);
|
||||
|
||||
|
|
|
@ -5,59 +5,9 @@ import Combine from "../UI/Base/Combine";
|
|||
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>
|
||||
|
||||
<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>
|
||||
|
||||
|
@ -71,11 +21,97 @@ Before proceeding, some warnings:
|
|||
In the layer object, add a field <b>calculatedTags</b>, e.g.:
|
||||
|
||||
<div class="code">
|
||||
"calculatedTags": {
|
||||
"_someKey": "javascript-expression",
|
||||
"name": "feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",
|
||||
"_distanceCloserThen3Km": "feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'"
|
||||
}
|
||||
"calculatedTags": [
|
||||
"_someKey=javascript-expression",
|
||||
"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",
|
||||
"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'"
|
||||
]
|
||||
</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 RememberingSource from "../FeatureSource/RememberingSource";
|
||||
import WayHandlingApplyingFeatureSource from "../FeatureSource/WayHandlingApplyingFeatureSource";
|
||||
import NoOverlapSource from "../FeatureSource/NoOverlapSource";
|
||||
import FeatureDuplicatorPerLayer from "../FeatureSource/FeatureDuplicatorPerLayer";
|
||||
import FeatureSource from "../FeatureSource/FeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
|
@ -25,9 +24,8 @@ export default class FeaturePipeline implements FeatureSource {
|
|||
locationControl: UIEventSource<Loc>) {
|
||||
|
||||
const amendedOverpassSource =
|
||||
new RememberingSource(
|
||||
new NoOverlapSource(flayers, new FeatureDuplicatorPerLayer(flayers,
|
||||
new LocalStorageSaver(updater, layout)))
|
||||
new RememberingSource(new FeatureDuplicatorPerLayer(flayers,
|
||||
new LocalStorageSaver(updater, layout))
|
||||
);
|
||||
|
||||
const geojsonSources: GeoJsonSource [] = []
|
||||
|
@ -40,8 +38,7 @@ export default class FeaturePipeline implements FeatureSource {
|
|||
}
|
||||
|
||||
const amendedLocalStorageSource =
|
||||
new RememberingSource(
|
||||
new NoOverlapSource(flayers, new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout)))
|
||||
new RememberingSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout))
|
||||
);
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
static featureIsContainedInAny(feature: any,
|
||||
shouldNotContain: any[],
|
||||
maxOverlapPercentage: number): boolean {
|
||||
// Returns 'false' if no problematic intersection is found
|
||||
/**
|
||||
* Calculates the overlap of 'feature' with every other specified feature.
|
||||
* The features with which 'feature' overlaps, are returned together with their overlap area in m²
|
||||
*
|
||||
* 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") {
|
||||
const coor = feature.geometry.coordinates;
|
||||
for (const shouldNotContainElement of shouldNotContain) {
|
||||
for (const otherFeature of otherFeatures) {
|
||||
|
||||
let shouldNotContainBBox = BBox.get(shouldNotContainElement);
|
||||
let featureBBox = BBox.get(feature);
|
||||
if (!featureBBox.overlapsWith(shouldNotContainBBox)) {
|
||||
let otherFeatureBBox = BBox.get(otherFeature);
|
||||
if (!featureBBox.overlapsWith(otherFeatureBBox)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.inside(coor, shouldNotContainElement)) {
|
||||
return true
|
||||
if (this.inside(coor, otherFeatures)) {
|
||||
result.push({ feat: otherFeatures, overlap: undefined })
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") {
|
||||
|
||||
const poly = feature;
|
||||
let featureBBox = BBox.get(feature);
|
||||
const featureSurface = GeoOperations.surfaceAreaInSqMeters(poly);
|
||||
for (const shouldNotContainElement of shouldNotContain) {
|
||||
|
||||
const shouldNotContainBBox = BBox.get(shouldNotContainElement);
|
||||
const overlaps = featureBBox.overlapsWith(shouldNotContainBBox)
|
||||
for (const otherFeature of otherFeatures) {
|
||||
const otherFeatureBBox = BBox.get(otherFeature);
|
||||
const overlaps = featureBBox.overlapsWith(otherFeatureBBox)
|
||||
if (!overlaps) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate the surface area of the intersection
|
||||
// If it is too big, refuse
|
||||
try {
|
||||
|
||||
const intersection = turf.intersect(poly, shouldNotContainElement);
|
||||
const intersection = turf.intersect(feature, otherFeature);
|
||||
if (intersection == null) {
|
||||
continue;
|
||||
}
|
||||
const intersectionSize = turf.area(intersection);
|
||||
const ratio = intersectionSize / featureSurface;
|
||||
|
||||
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;
|
||||
}
|
||||
const intersectionSize = turf.area(intersection); // in m²
|
||||
result.push({feat: otherFeature, overlap: intersectionSize})
|
||||
} catch (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;
|
||||
}
|
||||
|
||||
return false;
|
||||
console.error("Could not correctly calculate the overlap of ", feature, ": unsupported type")
|
||||
return result;
|
||||
}
|
||||
|
||||
public static inside(pointCoordinate, feature): boolean {
|
||||
// ray-casting algorithm based on
|
||||
// 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
|
||||
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) {
|
||||
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) {
|
||||
// @ts-ignore
|
||||
const key = feature.feature._matching_layer_id;
|
||||
|
@ -39,19 +49,19 @@ export default class MetaTagging {
|
|||
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;
|
||||
if (calculatedTags === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const functions: ((feature: any) => void)[] = [];
|
||||
const functions: ((featuresPerLayer: Map<string, any[]>, feature: any) => void)[] = [];
|
||||
for (const entry of calculatedTags) {
|
||||
const key = entry[0]
|
||||
const code = entry[1];
|
||||
|
@ -61,26 +71,24 @@ export default class MetaTagging {
|
|||
|
||||
const func = new Function("feat", "return " + code + ";");
|
||||
|
||||
const f = (feature: any) => {
|
||||
const f = (featuresPerLayer, feature: any) => {
|
||||
feature.properties[key] = func(feature);
|
||||
}
|
||||
functions.push(f)
|
||||
}
|
||||
return (feature) => {
|
||||
return (featuresPerLayer: Map<string, any[]>, feature) => {
|
||||
const tags = feature.properties
|
||||
if (tags === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExtraFunction.FullPatchFeature(feature);
|
||||
|
||||
for (const f of functions) {
|
||||
try {
|
||||
f(feature);
|
||||
} catch (e) {
|
||||
console.error("While calculating a tag value: ", e)
|
||||
ExtraFunction.FullPatchFeature(featuresPerLayer, feature);
|
||||
try {
|
||||
for (const f of functions) {
|
||||
f(featuresPerLayer, feature);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("While calculating a tag value: ", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ export default class SimpleMetaTagger {
|
|||
const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature);
|
||||
feature.properties["_surface"] = "" + sqMeters;
|
||||
feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10;
|
||||
|
||||
feature.area = sqMeters;
|
||||
})
|
||||
);
|
||||
private static country = new SimpleMetaTagger(
|
||||
|
|
|
@ -5,13 +5,13 @@ import * as $ from "jquery"
|
|||
*/
|
||||
export class Wikimedia {
|
||||
|
||||
private static knownLicenses = {};
|
||||
|
||||
static ImageNameToUrl(filename: string, width: number = 500, height: number = 200): string {
|
||||
filename = encodeURIComponent(filename);
|
||||
return "https://commons.wikimedia.org/wiki/Special:FilePath/" + filename + "?width=" + width + "&height=" + height;
|
||||
}
|
||||
|
||||
private static knownLicenses = {};
|
||||
|
||||
static LicenseData(filename: string, handle: ((LicenseInfo) => void)): void {
|
||||
if (filename in this.knownLicenses) {
|
||||
return this.knownLicenses[filename];
|
||||
|
@ -42,8 +42,9 @@ export class Wikimedia {
|
|||
|
||||
}
|
||||
|
||||
static GetCategoryFiles(categoryName: string, handleCategory: ((ImagesInCategory) => void),
|
||||
alreadyLoaded = 0, continueParameter: { k: string, param: string } = undefined) {
|
||||
static GetCategoryFiles(categoryName: string, handleCategory: ((ImagesInCategory: ImagesInCategory) => void),
|
||||
alreadyLoaded = 0,
|
||||
continueParameter: { k: string, param: string } = undefined) {
|
||||
if (categoryName === undefined || categoryName === null || categoryName === "") {
|
||||
return;
|
||||
}
|
||||
|
@ -58,7 +59,8 @@ export class Wikimedia {
|
|||
if (continueParameter !== undefined) {
|
||||
url = url + "&" + continueParameter.k + "=" + continueParameter.param;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
console.log("Loading a wikimedia category: ", url)
|
||||
$.getJSON(url, (response) => {
|
||||
let imageOverview = new ImagesInCategory();
|
||||
let members = response.query?.categorymembers;
|
||||
|
@ -67,21 +69,27 @@ export class Wikimedia {
|
|||
}
|
||||
|
||||
for (const member of members) {
|
||||
|
||||
imageOverview.images.push(member.title);
|
||||
}
|
||||
if (response.continue === undefined || alreadyLoaded > 30) {
|
||||
console.log("Got images! ", imageOverview)
|
||||
if (response.continue === undefined) {
|
||||
handleCategory(imageOverview);
|
||||
} else {
|
||||
console.log("Recursive load for ", categoryName)
|
||||
this.GetCategoryFiles(categoryName, (recursiveImages) => {
|
||||
for (const image of imageOverview.images) {
|
||||
recursiveImages.images.push(image);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (alreadyLoaded > 10) {
|
||||
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);
|
||||
},
|
||||
alreadyLoaded + 10, {k: "cmcontinue", param: response.continue.cmcontinue})
|
||||
}
|
||||
alreadyLoaded + 10,
|
||||
{k: "cmcontinue", param: response.continue.cmcontinue})
|
||||
|
||||
});
|
||||
}
|
||||
|
@ -102,8 +110,7 @@ export class Wikimedia {
|
|||
handleWikidata(wd);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -98,10 +98,6 @@ export default class LayerPanel extends UIElement {
|
|||
{value: 2, shown: "Show both the ways/areas and the centerpoints"},
|
||||
{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"),
|
||||
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",
|
||||
"The tags of the objects to load from overpass"),
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ export default class SpecialVisualizations {
|
|||
constr: (state: State, tags, args) => {
|
||||
const imagePrefix = args[0];
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"startLat": 50.8435,
|
||||
"startLon": 4.3688,
|
||||
"startZoom": 16,
|
||||
"widenFactor": 0.05,
|
||||
"widenFactor": 0.01,
|
||||
"socialImage": "./assets/themes/buurtnatuur/social_image.jpg",
|
||||
"layers": [
|
||||
{
|
||||
|
@ -75,7 +75,6 @@
|
|||
"tagRenderings": [
|
||||
"images"
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 10,
|
||||
"icon": {
|
||||
"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": {
|
||||
"render": {
|
||||
"nl": "Park"
|
||||
|
@ -149,7 +161,7 @@
|
|||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"name:nl~"
|
||||
"name:nl~*"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
|
@ -174,7 +186,6 @@
|
|||
"tagRenderings": [
|
||||
"images"
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 10,
|
||||
"icon": {
|
||||
"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": {
|
||||
"render": {
|
||||
"nl": "Bos"
|
||||
|
@ -236,7 +260,7 @@
|
|||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"name:nl~"
|
||||
"name:nl~*"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
|
@ -261,7 +285,6 @@
|
|||
"tagRenderings": [
|
||||
"images"
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 0,
|
||||
"icon": {
|
||||
"render": "circle:#ffffff;./assets/themes/buurtnatuur/forest.svg"
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue