forked from MapComplete/MapComplete
		
	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…
	
	Add table
		Add a link
		
	
		Reference in a new issue