forked from MapComplete/MapComplete
		
	Reformat all files with prettier
This commit is contained in:
		
							parent
							
								
									e22d189376
								
							
						
					
					
						commit
						b541d3eab4
					
				
					 382 changed files with 50893 additions and 35566 deletions
				
			
		|  | @ -1,26 +1,28 @@ | |||
| import * as known_themes from "../assets/generated/known_layers_and_themes.json" | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| import BaseUIElement from "../UI/BaseUIElement"; | ||||
| import Combine from "../UI/Base/Combine"; | ||||
| import Title from "../UI/Base/Title"; | ||||
| import List from "../UI/Base/List"; | ||||
| import DependencyCalculator from "../Models/ThemeConfig/DependencyCalculator"; | ||||
| import Constants from "../Models/Constants"; | ||||
| import {Utils} from "../Utils"; | ||||
| import Link from "../UI/Base/Link"; | ||||
| import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; | ||||
| import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||
| import BaseUIElement from "../UI/BaseUIElement" | ||||
| import Combine from "../UI/Base/Combine" | ||||
| import Title from "../UI/Base/Title" | ||||
| import List from "../UI/Base/List" | ||||
| import DependencyCalculator from "../Models/ThemeConfig/DependencyCalculator" | ||||
| import Constants from "../Models/Constants" | ||||
| import { Utils } from "../Utils" | ||||
| import Link from "../UI/Base/Link" | ||||
| import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" | ||||
| import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" | ||||
| 
 | ||||
| export class AllKnownLayouts { | ||||
|     public static allKnownLayouts: Map<string, LayoutConfig> = AllKnownLayouts.AllLayouts(); | ||||
|     public static layoutsList: LayoutConfig[] = AllKnownLayouts.GenerateOrderedList(AllKnownLayouts.allKnownLayouts); | ||||
|     public static allKnownLayouts: Map<string, LayoutConfig> = AllKnownLayouts.AllLayouts() | ||||
|     public static layoutsList: LayoutConfig[] = AllKnownLayouts.GenerateOrderedList( | ||||
|         AllKnownLayouts.allKnownLayouts | ||||
|     ) | ||||
|     // Must be below the list...
 | ||||
|     private static sharedLayers: Map<string, LayerConfig> = AllKnownLayouts.getSharedLayers(); | ||||
|     private static sharedLayers: Map<string, LayerConfig> = AllKnownLayouts.getSharedLayers() | ||||
| 
 | ||||
|     public static AllPublicLayers(options?: { | ||||
|         includeInlineLayers:true | boolean | ||||
|     }) : LayerConfig[] { | ||||
|         includeInlineLayers: true | boolean | ||||
|     }): LayerConfig[] { | ||||
|         const allLayers: LayerConfig[] = [] | ||||
|         const seendIds = new Set<string>() | ||||
|         AllKnownLayouts.sharedLayers.forEach((layer, key) => { | ||||
|  | @ -28,7 +30,7 @@ export class AllKnownLayouts { | |||
|             allLayers.push(layer) | ||||
|         }) | ||||
|         if (options?.includeInlineLayers ?? true) { | ||||
|             const publicLayouts = AllKnownLayouts.layoutsList.filter(l => !l.hideFromOverview) | ||||
|             const publicLayouts = AllKnownLayouts.layoutsList.filter((l) => !l.hideFromOverview) | ||||
|             for (const layout of publicLayouts) { | ||||
|                 if (layout.hideFromOverview) { | ||||
|                     continue | ||||
|  | @ -40,7 +42,6 @@ export class AllKnownLayouts { | |||
|                     seendIds.add(layer.id) | ||||
|                     allLayers.push(layer) | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -52,11 +53,14 @@ export class AllKnownLayouts { | |||
|      */ | ||||
|     public static themesUsingLayer(id: string, publicOnly = true): LayoutConfig[] { | ||||
|         const themes = AllKnownLayouts.layoutsList | ||||
|             .filter(l => !(publicOnly && l.hideFromOverview) && l.id !== "personal") | ||||
|             .map(theme => ({theme, minzoom: theme.layers.find(layer => layer.id === id)?.minzoom})) | ||||
|             .filter(obj => obj.minzoom !== undefined) | ||||
|             .filter((l) => !(publicOnly && l.hideFromOverview) && l.id !== "personal") | ||||
|             .map((theme) => ({ | ||||
|                 theme, | ||||
|                 minzoom: theme.layers.find((layer) => layer.id === id)?.minzoom, | ||||
|             })) | ||||
|             .filter((obj) => obj.minzoom !== undefined) | ||||
|         themes.sort((th0, th1) => th1.minzoom - th0.minzoom) | ||||
|         return themes.map(th => th.theme); | ||||
|         return themes.map((th) => th.theme) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -65,12 +69,15 @@ export class AllKnownLayouts { | |||
|      * @param callback | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static GenOverviewsForSingleLayer(callback: (layer: LayerConfig, element: BaseUIElement, inlineSource: string) => void): void { | ||||
|         const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()) | ||||
|             .filter(layer => Constants.priviliged_layers.indexOf(layer.id) < 0) | ||||
|     public static GenOverviewsForSingleLayer( | ||||
|         callback: (layer: LayerConfig, element: BaseUIElement, inlineSource: string) => void | ||||
|     ): void { | ||||
|         const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()).filter( | ||||
|             (layer) => Constants.priviliged_layers.indexOf(layer.id) < 0 | ||||
|         ) | ||||
|         const builtinLayerIds: Set<string> = new Set<string>() | ||||
|         allLayers.forEach(l => builtinLayerIds.add(l.id)) | ||||
|         const inlineLayers = new Map<string, string>(); | ||||
|         allLayers.forEach((l) => builtinLayerIds.add(l.id)) | ||||
|         const inlineLayers = new Map<string, string>() | ||||
| 
 | ||||
|         for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) { | ||||
|             if (layout.hideFromOverview) { | ||||
|  | @ -78,7 +85,6 @@ export class AllKnownLayouts { | |||
|             } | ||||
| 
 | ||||
|             for (const layer of layout.layers) { | ||||
| 
 | ||||
|                 if (Constants.priviliged_layers.indexOf(layer.id) >= 0) { | ||||
|                     continue | ||||
|                 } | ||||
|  | @ -113,7 +119,6 @@ export class AllKnownLayouts { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         // Determine the cross-dependencies
 | ||||
|         const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>() | ||||
| 
 | ||||
|  | @ -125,12 +130,14 @@ export class AllKnownLayouts { | |||
|                 } | ||||
|                 layerIsNeededBy.get(dependency).push(layer.id) | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         allLayers.forEach((layer) => { | ||||
|             const element = layer.GenerateDocumentation(themesPerLayer.get(layer.id), layerIsNeededBy, DependencyCalculator.getLayerDependencies(layer)) | ||||
|             const element = layer.GenerateDocumentation( | ||||
|                 themesPerLayer.get(layer.id), | ||||
|                 layerIsNeededBy, | ||||
|                 DependencyCalculator.getLayerDependencies(layer) | ||||
|             ) | ||||
|             callback(layer, element, inlineLayers.get(layer.id)) | ||||
|         }) | ||||
|     } | ||||
|  | @ -146,11 +153,12 @@ export class AllKnownLayouts { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()) | ||||
|             .filter(layer => Constants.priviliged_layers.indexOf(layer.id) < 0) | ||||
|         const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()).filter( | ||||
|             (layer) => Constants.priviliged_layers.indexOf(layer.id) < 0 | ||||
|         ) | ||||
| 
 | ||||
|         const builtinLayerIds: Set<string> = new Set<string>() | ||||
|         allLayers.forEach(l => builtinLayerIds.add(l.id)) | ||||
|         allLayers.forEach((l) => builtinLayerIds.add(l.id)) | ||||
| 
 | ||||
|         const themesPerLayer = new Map<string, string[]>() | ||||
| 
 | ||||
|  | @ -166,7 +174,6 @@ export class AllKnownLayouts { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         // Determine the cross-dependencies
 | ||||
|         const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>() | ||||
| 
 | ||||
|  | @ -178,25 +185,32 @@ export class AllKnownLayouts { | |||
|                 } | ||||
|                 layerIsNeededBy.get(dependency).push(layer.id) | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         return new Combine([ | ||||
|             new Title("Special and other useful layers", 1), | ||||
|             "MapComplete has a few data layers available in the theme which have special properties through builtin-hooks. Furthermore, there are some normal layers (which are built from normal Theme-config files) but are so general that they get a mention here.", | ||||
|             new Title("Priviliged layers", 1), | ||||
|             new List(Constants.priviliged_layers.map(id => "[" + id + "](#" + id + ")")), | ||||
|             new List(Constants.priviliged_layers.map((id) => "[" + id + "](#" + id + ")")), | ||||
|             ...Constants.priviliged_layers | ||||
|                 .map(id => AllKnownLayouts.sharedLayers.get(id)) | ||||
|                 .map((l) => l.GenerateDocumentation(themesPerLayer.get(l.id), layerIsNeededBy, DependencyCalculator.getLayerDependencies(l), Constants.added_by_default.indexOf(l.id) >= 0, Constants.no_include.indexOf(l.id) < 0)), | ||||
|                 .map((id) => AllKnownLayouts.sharedLayers.get(id)) | ||||
|                 .map((l) => | ||||
|                     l.GenerateDocumentation( | ||||
|                         themesPerLayer.get(l.id), | ||||
|                         layerIsNeededBy, | ||||
|                         DependencyCalculator.getLayerDependencies(l), | ||||
|                         Constants.added_by_default.indexOf(l.id) >= 0, | ||||
|                         Constants.no_include.indexOf(l.id) < 0 | ||||
|                     ) | ||||
|                 ), | ||||
|             new Title("Normal layers", 1), | ||||
|             "The following layers are included in MapComplete:", | ||||
|             new List(Array.from(AllKnownLayouts.sharedLayers.keys()).map(id => new Link(id, "./Layers/" + id + ".md"))) | ||||
|             new List( | ||||
|                 Array.from(AllKnownLayouts.sharedLayers.keys()).map( | ||||
|                     (id) => new Link(id, "./Layers/" + id + ".md") | ||||
|                 ) | ||||
|             ), | ||||
|         ]) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public static GenerateDocumentationForTheme(theme: LayoutConfig): BaseUIElement { | ||||
|  | @ -204,37 +218,42 @@ export class AllKnownLayouts { | |||
|             new Title(new Combine([theme.title, "(", theme.id + ")"]), 2), | ||||
|             theme.description, | ||||
|             "This theme contains the following layers:", | ||||
|             new List(theme.layers.map(l => l.id)), | ||||
|             new List(theme.layers.map((l) => l.id)), | ||||
|             "Available languages:", | ||||
|             new List(theme.language) | ||||
|             new List(theme.language), | ||||
|         ]) | ||||
|     } | ||||
| 
 | ||||
|     public static getSharedLayers(): Map<string, LayerConfig> { | ||||
|         const sharedLayers = new Map<string, LayerConfig>(); | ||||
|         const sharedLayers = new Map<string, LayerConfig>() | ||||
|         for (const layer of known_themes["layers"]) { | ||||
|             try { | ||||
|                 // @ts-ignore
 | ||||
|                 const parsed = new LayerConfig(layer, "shared_layers") | ||||
|                 sharedLayers.set(layer.id, parsed); | ||||
|                 sharedLayers.set(layer.id, parsed) | ||||
|             } catch (e) { | ||||
|                 if (!Utils.runningFromConsole) { | ||||
|                     console.error("CRITICAL: Could not parse a layer configuration!", layer.id, " due to", e) | ||||
|                     console.error( | ||||
|                         "CRITICAL: Could not parse a layer configuration!", | ||||
|                         layer.id, | ||||
|                         " due to", | ||||
|                         e | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return sharedLayers; | ||||
|         return sharedLayers | ||||
|     } | ||||
| 
 | ||||
|     public static getSharedLayersConfigs(): Map<string, LayerConfigJson> { | ||||
|         const sharedLayers = new Map<string, LayerConfigJson>(); | ||||
|         const sharedLayers = new Map<string, LayerConfigJson>() | ||||
|         for (const layer of known_themes["layers"]) { | ||||
|             // @ts-ignore
 | ||||
|                 sharedLayers.set(layer.id, layer); | ||||
|             sharedLayers.set(layer.id, layer) | ||||
|         } | ||||
| 
 | ||||
|         return sharedLayers; | ||||
|         return sharedLayers | ||||
|     } | ||||
| 
 | ||||
|     private static GenerateOrderedList(allKnownLayouts: Map<string, LayoutConfig>): LayoutConfig[] { | ||||
|  | @ -242,28 +261,26 @@ export class AllKnownLayouts { | |||
|         allKnownLayouts.forEach((layout) => { | ||||
|             list.push(layout) | ||||
|         }) | ||||
|         return list; | ||||
|         return list | ||||
|     } | ||||
| 
 | ||||
|     private static AllLayouts(): Map<string, LayoutConfig> { | ||||
|         const dict: Map<string, LayoutConfig> = new Map(); | ||||
|         const dict: Map<string, LayoutConfig> = new Map() | ||||
|         for (const layoutConfigJson of known_themes["themes"]) { | ||||
|             const layout = new LayoutConfig(<LayoutConfigJson>layoutConfigJson, true) | ||||
|             dict.set(layout.id, layout) | ||||
|             for (let i = 0; i < layout.layers.length; i++) { | ||||
|                 let layer = layout.layers[i]; | ||||
|                 if (typeof (layer) === "string") { | ||||
|                     layer = AllKnownLayouts.sharedLayers.get(layer); | ||||
|                 let layer = layout.layers[i] | ||||
|                 if (typeof layer === "string") { | ||||
|                     layer = AllKnownLayouts.sharedLayers.get(layer) | ||||
|                     layout.layers[i] = layer | ||||
|                     if (layer === undefined) { | ||||
|                         console.log("Defined layers are ", AllKnownLayouts.sharedLayers.keys()) | ||||
|                         throw `Layer ${layer} was not found or defined - probably a type was made` | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|         } | ||||
|         return dict; | ||||
|         return dict | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,38 +1,49 @@ | |||
| import * as questions from "../assets/tagRenderings/questions.json"; | ||||
| import * as icons from "../assets/tagRenderings/icons.json"; | ||||
| import {Utils} from "../Utils"; | ||||
| import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; | ||||
| import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; | ||||
| import BaseUIElement from "../UI/BaseUIElement"; | ||||
| import Combine from "../UI/Base/Combine"; | ||||
| import Title from "../UI/Base/Title"; | ||||
| import {FixedUiElement} from "../UI/Base/FixedUiElement"; | ||||
| import List from "../UI/Base/List"; | ||||
| import * as questions from "../assets/tagRenderings/questions.json" | ||||
| import * as icons from "../assets/tagRenderings/icons.json" | ||||
| import { Utils } from "../Utils" | ||||
| import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" | ||||
| import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson" | ||||
| import BaseUIElement from "../UI/BaseUIElement" | ||||
| import Combine from "../UI/Base/Combine" | ||||
| import Title from "../UI/Base/Title" | ||||
| import { FixedUiElement } from "../UI/Base/FixedUiElement" | ||||
| import List from "../UI/Base/List" | ||||
| 
 | ||||
| export default class SharedTagRenderings { | ||||
| 
 | ||||
|     public static SharedTagRendering: Map<string, TagRenderingConfig> = SharedTagRenderings.generatedSharedFields(); | ||||
|     public static SharedTagRenderingJson: Map<string, TagRenderingConfigJson> = SharedTagRenderings.generatedSharedFieldsJsons(); | ||||
|     public static SharedIcons: Map<string, TagRenderingConfig> = SharedTagRenderings.generatedSharedFields(true); | ||||
|     public static SharedTagRendering: Map<string, TagRenderingConfig> = | ||||
|         SharedTagRenderings.generatedSharedFields() | ||||
|     public static SharedTagRenderingJson: Map<string, TagRenderingConfigJson> = | ||||
|         SharedTagRenderings.generatedSharedFieldsJsons() | ||||
|     public static SharedIcons: Map<string, TagRenderingConfig> = | ||||
|         SharedTagRenderings.generatedSharedFields(true) | ||||
| 
 | ||||
|     private static generatedSharedFields(iconsOnly = false): Map<string, TagRenderingConfig> { | ||||
|         const configJsons = SharedTagRenderings.generatedSharedFieldsJsons(iconsOnly) | ||||
|         const d = new Map<string, TagRenderingConfig>() | ||||
|         for (const key of Array.from(configJsons.keys())) { | ||||
|             try { | ||||
|                 d.set(key, new TagRenderingConfig(configJsons.get(key), `SharedTagRenderings.${key}`)) | ||||
|                 d.set( | ||||
|                     key, | ||||
|                     new TagRenderingConfig(configJsons.get(key), `SharedTagRenderings.${key}`) | ||||
|                 ) | ||||
|             } catch (e) { | ||||
|                 if (!Utils.runningFromConsole) { | ||||
|                     console.error("BUG: could not parse", key, " from questions.json or icons.json - this error happened during the build step of the SharedTagRenderings", e) | ||||
| 
 | ||||
|                     console.error( | ||||
|                         "BUG: could not parse", | ||||
|                         key, | ||||
|                         " from questions.json or icons.json - this error happened during the build step of the SharedTagRenderings", | ||||
|                         e | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return d | ||||
|     } | ||||
| 
 | ||||
|     private static generatedSharedFieldsJsons(iconsOnly = false): Map<string, TagRenderingConfigJson> { | ||||
|         const dict = new Map<string, TagRenderingConfigJson>(); | ||||
|     private static generatedSharedFieldsJsons( | ||||
|         iconsOnly = false | ||||
|     ): Map<string, TagRenderingConfigJson> { | ||||
|         const dict = new Map<string, TagRenderingConfigJson>() | ||||
| 
 | ||||
|         if (!iconsOnly) { | ||||
|             for (const key in questions) { | ||||
|  | @ -53,13 +64,16 @@ export default class SharedTagRenderings { | |||
|             if (key === "id") { | ||||
|                 return | ||||
|             } | ||||
|             value.id = value.id ?? key; | ||||
|             if(value["builtin"] !== undefined){ | ||||
|                 if(value["override"] == undefined){ | ||||
|                     throw "HUH? Why whould you want to reuse a builtin if one doesn't override? In questions.json/"+key | ||||
|             value.id = value.id ?? key | ||||
|             if (value["builtin"] !== undefined) { | ||||
|                 if (value["override"] == undefined) { | ||||
|                     throw ( | ||||
|                         "HUH? Why whould you want to reuse a builtin if one doesn't override? In questions.json/" + | ||||
|                         key | ||||
|                     ) | ||||
|                 } | ||||
|                 if(typeof value["builtin"] !== "string"){ | ||||
|                     return; | ||||
|                 if (typeof value["builtin"] !== "string") { | ||||
|                     return | ||||
|                 } | ||||
|                 // This is a really funny situation: we extend another tagRendering!
 | ||||
|                 const parent = Utils.Clone(dict.get(value["builtin"])) | ||||
|  | @ -73,36 +87,31 @@ export default class SharedTagRenderings { | |||
|             } | ||||
|         }) | ||||
| 
 | ||||
|          | ||||
|         return dict; | ||||
|         return dict | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static HelpText(): BaseUIElement { | ||||
|         return new Combine([ | ||||
|             new Combine([ | ||||
|                 new Title("Builtin questions", 1), | ||||
| 
 | ||||
|                 new Title("Builtin questions",1), | ||||
| 
 | ||||
|                 "The following items can be easily reused in your layers" | ||||
|                 "The following items can be easily reused in your layers", | ||||
|             ]).SetClass("flex flex-col"), | ||||
| 
 | ||||
|             ... Array.from( SharedTagRenderings.SharedTagRendering.keys()).map(key => { | ||||
|             ...Array.from(SharedTagRenderings.SharedTagRendering.keys()).map((key) => { | ||||
|                 const tr = SharedTagRenderings.SharedTagRendering.get(key) | ||||
|                 let mappings: BaseUIElement = undefined | ||||
|                 if(tr.mappings?.length > 0){ | ||||
|                     mappings = new List(tr.mappings.map(m => m.then.textFor("en"))) | ||||
|                 if (tr.mappings?.length > 0) { | ||||
|                     mappings = new List(tr.mappings.map((m) => m.then.textFor("en"))) | ||||
|                 } | ||||
|                 return new Combine([ | ||||
|                     new Title(key), | ||||
|                     tr.render?.textFor("en"), | ||||
|                     tr.question?.textFor("en") ?? new FixedUiElement("Read-only tagrendering").SetClass("font-bold"), | ||||
|                     mappings | ||||
|                     tr.question?.textFor("en") ?? | ||||
|                         new FixedUiElement("Read-only tagrendering").SetClass("font-bold"), | ||||
|                     mappings, | ||||
|                 ]).SetClass("flex flex-col") | ||||
| 
 | ||||
|             }) | ||||
| 
 | ||||
|             }), | ||||
|         ]).SetClass("flex flex-col") | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,29 +1,27 @@ | |||
| import {existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync} from "fs"; | ||||
| import ScriptUtils from "../../scripts/ScriptUtils"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs" | ||||
| import ScriptUtils from "../../scripts/ScriptUtils" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| ScriptUtils.fixUtils() | ||||
| 
 | ||||
| class StatsDownloader { | ||||
|     private readonly urlTemplate = | ||||
|         "https://osmcha.org/api/v1/changesets/?date__gte={start_date}&date__lte={end_date}&page={page}&comment=%23mapcomplete&page_size=100" | ||||
| 
 | ||||
|     private readonly urlTemplate = "https://osmcha.org/api/v1/changesets/?date__gte={start_date}&date__lte={end_date}&page={page}&comment=%23mapcomplete&page_size=100" | ||||
| 
 | ||||
|     private readonly _targetDirectory: string; | ||||
|     private readonly _targetDirectory: string | ||||
| 
 | ||||
|     constructor(targetDirectory = ".") { | ||||
|         this._targetDirectory = targetDirectory; | ||||
|         this._targetDirectory = targetDirectory | ||||
|     } | ||||
| 
 | ||||
|     public async DownloadStats(startYear = 2020, startMonth = 5, startDay = 1) { | ||||
| 
 | ||||
|         const today = new Date(); | ||||
|         const today = new Date() | ||||
|         const currentYear = today.getFullYear() | ||||
|         const currentMonth = today.getMonth() + 1 | ||||
|         for (let year = startYear; year <= currentYear; year++) { | ||||
|             for (let month = 1; month <= 12; month++) { | ||||
| 
 | ||||
|                 if (year === startYear && month < startMonth) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 if (year === currentYear && month > currentMonth) { | ||||
|  | @ -32,33 +30,40 @@ class StatsDownloader { | |||
| 
 | ||||
|                 const pathM = `${this._targetDirectory}/stats.${year}-${month}.json` | ||||
|                 if (existsSync(pathM)) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 const features = [] | ||||
|                 let monthIsFinished = true | ||||
|                 const writtenFiles = [] | ||||
|                 for (let day = startDay; day <= 31; day++) { | ||||
|                      | ||||
|                     if (year === currentYear && month === currentMonth && day === today.getDate()) { | ||||
|                         monthIsFinished = false | ||||
|                         break; | ||||
|                         break | ||||
|                     } | ||||
|                     { | ||||
|                         const date = new Date(year, month - 1, day) | ||||
|                         if(date.getMonth() != month -1){ | ||||
|                         if (date.getMonth() != month - 1) { | ||||
|                             // We did roll over
 | ||||
|                             continue | ||||
|                         } | ||||
|                     } | ||||
|                     const path = `${this._targetDirectory}/stats.${year}-${month}-${(day < 10 ? "0" : "") + day}.day.json` | ||||
|                     const path = `${this._targetDirectory}/stats.${year}-${month}-${ | ||||
|                         (day < 10 ? "0" : "") + day | ||||
|                     }.day.json` | ||||
|                     writtenFiles.push(path) | ||||
|                     if (existsSync(path)) { | ||||
|                         let features = JSON.parse(readFileSync(path, "UTF-8")) | ||||
|                         features = features?.features ?? features | ||||
|                         console.log(features) | ||||
|                         features.push(...features.features ) // day-stats are generally a list already, but in some ad-hoc cases might be a geojson-collection too
 | ||||
|                         console.log("Loaded ", path, "from disk, got", features.length, "features now") | ||||
|                         features.push(...features.features) // day-stats are generally a list already, but in some ad-hoc cases might be a geojson-collection too
 | ||||
|                         console.log( | ||||
|                             "Loaded ", | ||||
|                             path, | ||||
|                             "from disk, got", | ||||
|                             features.length, | ||||
|                             "features now" | ||||
|                         ) | ||||
|                         continue | ||||
|                     } | ||||
|                     let dayFeatures: any[] = undefined | ||||
|  | @ -66,15 +71,22 @@ class StatsDownloader { | |||
|                         dayFeatures = await this.DownloadStatsForDay(year, month, day, path) | ||||
|                     } catch (e) { | ||||
|                         console.error(e) | ||||
|                         console.error("Could not download " + year + "-" + month + "-" + day + "... Trying again") | ||||
|                         console.error( | ||||
|                             "Could not download " + | ||||
|                                 year + | ||||
|                                 "-" + | ||||
|                                 month + | ||||
|                                 "-" + | ||||
|                                 day + | ||||
|                                 "... Trying again" | ||||
|                         ) | ||||
|                         dayFeatures = await this.DownloadStatsForDay(year, month, day, path) | ||||
|                     } | ||||
|                     writeFileSync(path, JSON.stringify(dayFeatures)) | ||||
|                     features.push(...dayFeatures) | ||||
| 
 | ||||
|                 } | ||||
|                 if(monthIsFinished){ | ||||
|                     writeFileSync(pathM, JSON.stringify({features})) | ||||
|                 if (monthIsFinished) { | ||||
|                     writeFileSync(pathM, JSON.stringify({ features })) | ||||
|                     for (const writtenFile of writtenFiles) { | ||||
|                         unlinkSync(writtenFile) | ||||
|                     } | ||||
|  | @ -82,37 +94,49 @@ class StatsDownloader { | |||
|             } | ||||
|             startDay = 1 | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public async DownloadStatsForDay(year: number, month: number, day: number, path: string): Promise<any[]> { | ||||
| 
 | ||||
|         let page = 1; | ||||
|     public async DownloadStatsForDay( | ||||
|         year: number, | ||||
|         month: number, | ||||
|         day: number, | ||||
|         path: string | ||||
|     ): Promise<any[]> { | ||||
|         let page = 1 | ||||
|         let allFeatures = [] | ||||
|         let endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1); | ||||
|         let endDate = `${endDay.getFullYear()}-${Utils.TwoDigits(endDay.getMonth() + 1)}-${Utils.TwoDigits(endDay.getDate())}` | ||||
|         let url = this.urlTemplate.replace("{start_date}", year + "-" + Utils.TwoDigits(month) + "-" + Utils.TwoDigits(day)) | ||||
|         let endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1) | ||||
|         let endDate = `${endDay.getFullYear()}-${Utils.TwoDigits( | ||||
|             endDay.getMonth() + 1 | ||||
|         )}-${Utils.TwoDigits(endDay.getDate())}` | ||||
|         let url = this.urlTemplate | ||||
|             .replace( | ||||
|                 "{start_date}", | ||||
|                 year + "-" + Utils.TwoDigits(month) + "-" + Utils.TwoDigits(day) | ||||
|             ) | ||||
|             .replace("{end_date}", endDate) | ||||
|             .replace("{page}", "" + page) | ||||
| 
 | ||||
| 
 | ||||
|         let headers = { | ||||
|             'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0', | ||||
|             'Accept-Language': 'en-US,en;q=0.5', | ||||
|             'Referer': 'https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D', | ||||
|             'Content-Type': 'application/json', | ||||
|             'Authorization': 'Token 6e422e2afedb79ef66573982012000281f03dc91', | ||||
|             'DNT': '1', | ||||
|             'Connection': 'keep-alive', | ||||
|             'TE': 'Trailers', | ||||
|             'Pragma': 'no-cache', | ||||
|             'Cache-Control': 'no-cache' | ||||
|             "User-Agent": | ||||
|                 "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0", | ||||
|             "Accept-Language": "en-US,en;q=0.5", | ||||
|             Referer: | ||||
|                 "https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D", | ||||
|             "Content-Type": "application/json", | ||||
|             Authorization: "Token 6e422e2afedb79ef66573982012000281f03dc91", | ||||
|             DNT: "1", | ||||
|             Connection: "keep-alive", | ||||
|             TE: "Trailers", | ||||
|             Pragma: "no-cache", | ||||
|             "Cache-Control": "no-cache", | ||||
|         } | ||||
| 
 | ||||
|         while (url) { | ||||
|             ScriptUtils.erasableLog(`Downloading stats for ${year}-${month}-${day}, page ${page} ${url}`) | ||||
|             ScriptUtils.erasableLog( | ||||
|                 `Downloading stats for ${year}-${month}-${day}, page ${page} ${url}` | ||||
|             ) | ||||
|             const result = await Utils.downloadJson(url, headers) | ||||
|             page++; | ||||
|             page++ | ||||
|             allFeatures.push(...result.features) | ||||
|             if (result.features === undefined) { | ||||
|                 console.log("ERROR", result) | ||||
|  | @ -120,58 +144,59 @@ class StatsDownloader { | |||
|             } | ||||
|             url = result.next | ||||
|         } | ||||
|         console.log(`Writing ${allFeatures.length} features to `, path, Utils.Times(_ => " ", 80)) | ||||
|         console.log( | ||||
|             `Writing ${allFeatures.length} features to `, | ||||
|             path, | ||||
|             Utils.Times((_) => " ", 80) | ||||
|         ) | ||||
|         allFeatures = Utils.NoNull(allFeatures) | ||||
|         allFeatures.forEach(f => { | ||||
|             f.properties = {...f.properties, ...f.properties.metadata} | ||||
|         allFeatures.forEach((f) => { | ||||
|             f.properties = { ...f.properties, ...f.properties.metadata } | ||||
|             delete f.properties.metadata | ||||
|             f.properties.id = f.id | ||||
|         }) | ||||
|         return allFeatures | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| interface ChangeSetData { | ||||
|     "id": number, | ||||
|     "type": "Feature", | ||||
|     "geometry": { | ||||
|         "type": "Polygon", | ||||
|         "coordinates": [number, number][][] | ||||
|     }, | ||||
|     "properties": { | ||||
|         "check_user": null, | ||||
|         "reasons": [], | ||||
|         "tags": [], | ||||
|         "features": [], | ||||
|         "user": string, | ||||
|         "uid": string, | ||||
|         "editor": string, | ||||
|         "comment": string, | ||||
|         "comments_count": number, | ||||
|         "source": string, | ||||
|         "imagery_used": string, | ||||
|         "date": string, | ||||
|         "reviewed_features": [], | ||||
|         "create": number, | ||||
|         "modify": number, | ||||
|         "delete": number, | ||||
|         "area": number, | ||||
|         "is_suspect": boolean, | ||||
|         "harmful": any, | ||||
|         "checked": boolean, | ||||
|         "check_date": any, | ||||
|         "metadata": { | ||||
|             "host": string, | ||||
|             "theme": string, | ||||
|             "imagery": string, | ||||
|             "language": string | ||||
|     id: number | ||||
|     type: "Feature" | ||||
|     geometry: { | ||||
|         type: "Polygon" | ||||
|         coordinates: [number, number][][] | ||||
|     } | ||||
|     properties: { | ||||
|         check_user: null | ||||
|         reasons: [] | ||||
|         tags: [] | ||||
|         features: [] | ||||
|         user: string | ||||
|         uid: string | ||||
|         editor: string | ||||
|         comment: string | ||||
|         comments_count: number | ||||
|         source: string | ||||
|         imagery_used: string | ||||
|         date: string | ||||
|         reviewed_features: [] | ||||
|         create: number | ||||
|         modify: number | ||||
|         delete: number | ||||
|         area: number | ||||
|         is_suspect: boolean | ||||
|         harmful: any | ||||
|         checked: boolean | ||||
|         check_date: any | ||||
|         metadata: { | ||||
|             host: string | ||||
|             theme: string | ||||
|             imagery: string | ||||
|             language: string | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| async function main(): Promise<void> { | ||||
|     if (!existsSync("graphs")) { | ||||
|         mkdirSync("graphs") | ||||
|  | @ -181,43 +206,47 @@ async function main(): Promise<void> { | |||
|     let year = 2020 | ||||
|     let month = 5 | ||||
|     let day = 1 | ||||
|     if(!isNaN(Number(process.argv[2]))){ | ||||
|     if (!isNaN(Number(process.argv[2]))) { | ||||
|         year = Number(process.argv[2]) | ||||
|     } | ||||
|     if(!isNaN(Number(process.argv[3]))){ | ||||
|     if (!isNaN(Number(process.argv[3]))) { | ||||
|         month = Number(process.argv[3]) | ||||
|     } | ||||
| 
 | ||||
|     if(!isNaN(Number(process.argv[4]))){ | ||||
|     if (!isNaN(Number(process.argv[4]))) { | ||||
|         day = Number(process.argv[4]) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     do { | ||||
|         try { | ||||
| 
 | ||||
|             await new StatsDownloader(targetDir).DownloadStats(year, month, day) | ||||
|             break | ||||
|         } catch (e) { | ||||
|             console.log(e) | ||||
|         } | ||||
| 
 | ||||
|     } while (true) | ||||
|     const allPaths = readdirSync(targetDir) | ||||
|         .filter(p => p.startsWith("stats.") && p.endsWith(".json")); | ||||
|     let allFeatures: ChangeSetData[] = [].concat(...allPaths | ||||
|         .map(path => JSON.parse(readFileSync("Docs/Tools/stats/" + path, "utf-8")).features)); | ||||
|     allFeatures = allFeatures.filter(f => f?.properties !== undefined && (f.properties.editor === null || f.properties.editor.toLowerCase().startsWith("mapcomplete"))) | ||||
|     const allPaths = readdirSync(targetDir).filter( | ||||
|         (p) => p.startsWith("stats.") && p.endsWith(".json") | ||||
|     ) | ||||
|     let allFeatures: ChangeSetData[] = [].concat( | ||||
|         ...allPaths.map( | ||||
|             (path) => JSON.parse(readFileSync("Docs/Tools/stats/" + path, "utf-8")).features | ||||
|         ) | ||||
|     ) | ||||
|     allFeatures = allFeatures.filter( | ||||
|         (f) => | ||||
|             f?.properties !== undefined && | ||||
|             (f.properties.editor === null || | ||||
|                 f.properties.editor.toLowerCase().startsWith("mapcomplete")) | ||||
|     ) | ||||
| 
 | ||||
|     allFeatures = allFeatures.filter(f => f.properties.metadata?.theme !== "EMPTY CS") | ||||
|     allFeatures = allFeatures.filter((f) => f.properties.metadata?.theme !== "EMPTY CS") | ||||
| 
 | ||||
|     if (process.argv.indexOf("--no-graphs") >= 0) { | ||||
|         return | ||||
|     } | ||||
|     const allFiles = readdirSync("Docs/Tools/stats").filter(p => p.endsWith(".json")) | ||||
|     const allFiles = readdirSync("Docs/Tools/stats").filter((p) => p.endsWith(".json")) | ||||
|     writeFileSync("Docs/Tools/stats/file-overview.json", JSON.stringify(allFiles)) | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| main().then(_ => console.log("All done!")) | ||||
| 
 | ||||
| main().then((_) => console.log("All done!")) | ||||
|  |  | |||
|  | @ -1,15 +1,17 @@ | |||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import {ImmutableStore, Store, UIEventSource} from "../UIEventSource"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import BaseLayer from "../../Models/BaseLayer" | ||||
| import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" | ||||
| import Loc from "../../Models/Loc" | ||||
| 
 | ||||
| export interface AvailableBaseLayersObj { | ||||
|     readonly osmCarto: BaseLayer; | ||||
|     layerOverview: BaseLayer[]; | ||||
|     readonly osmCarto: BaseLayer | ||||
|     layerOverview: BaseLayer[] | ||||
| 
 | ||||
|     AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> | ||||
| 
 | ||||
|     SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: Store<string | string[]>): Store<BaseLayer>; | ||||
| 
 | ||||
|     SelectBestLayerAccordingTo( | ||||
|         location: Store<Loc>, | ||||
|         preferedCategory: Store<string | string[]> | ||||
|     ): Store<BaseLayer> | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -17,20 +19,28 @@ export interface AvailableBaseLayersObj { | |||
|  * Changes the basemap | ||||
|  */ | ||||
| export default class AvailableBaseLayers { | ||||
| 
 | ||||
| 
 | ||||
|     public static layerOverview: BaseLayer[]; | ||||
|     public static osmCarto: BaseLayer; | ||||
|     public static layerOverview: BaseLayer[] | ||||
|     public static osmCarto: BaseLayer | ||||
| 
 | ||||
|     private static implementation: AvailableBaseLayersObj | ||||
| 
 | ||||
|     static AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> { | ||||
|         return AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? new ImmutableStore<BaseLayer[]>([]); | ||||
|         return ( | ||||
|             AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? | ||||
|             new ImmutableStore<BaseLayer[]>([]) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     static SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: UIEventSource<string | string[]>): Store<BaseLayer> { | ||||
|         return AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(location, preferedCategory) ?? new ImmutableStore<BaseLayer>(undefined); | ||||
| 
 | ||||
|     static SelectBestLayerAccordingTo( | ||||
|         location: Store<Loc>, | ||||
|         preferedCategory: UIEventSource<string | string[]> | ||||
|     ): Store<BaseLayer> { | ||||
|         return ( | ||||
|             AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo( | ||||
|                 location, | ||||
|                 preferedCategory | ||||
|             ) ?? new ImmutableStore<BaseLayer>(undefined) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     public static implement(backend: AvailableBaseLayersObj) { | ||||
|  | @ -38,5 +48,4 @@ export default class AvailableBaseLayers { | |||
|         AvailableBaseLayers.osmCarto = backend.osmCarto | ||||
|         AvailableBaseLayers.implementation = backend | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,66 +1,77 @@ | |||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import {Store, Stores} from "../UIEventSource"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import {GeoOperations} from "../GeoOperations"; | ||||
| import * as editorlayerindex from "../../assets/editor-layer-index.json"; | ||||
| import * as L from "leaflet"; | ||||
| import {TileLayer} from "leaflet"; | ||||
| import * as X from "leaflet-providers"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {AvailableBaseLayersObj} from "./AvailableBaseLayers"; | ||||
| import {BBox} from "../BBox"; | ||||
| import BaseLayer from "../../Models/BaseLayer" | ||||
| import { Store, Stores } from "../UIEventSource" | ||||
| import Loc from "../../Models/Loc" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import * as editorlayerindex from "../../assets/editor-layer-index.json" | ||||
| import * as L from "leaflet" | ||||
| import { TileLayer } from "leaflet" | ||||
| import * as X from "leaflet-providers" | ||||
| import { Utils } from "../../Utils" | ||||
| import { AvailableBaseLayersObj } from "./AvailableBaseLayers" | ||||
| import { BBox } from "../BBox" | ||||
| 
 | ||||
| export default class AvailableBaseLayersImplementation implements AvailableBaseLayersObj { | ||||
| 
 | ||||
|     public readonly osmCarto: BaseLayer = | ||||
|         { | ||||
|     public readonly osmCarto: BaseLayer = { | ||||
|         id: "osm", | ||||
|         name: "OpenStreetMap", | ||||
|             layer: () => AvailableBaseLayersImplementation.CreateBackgroundLayer("osm", "OpenStreetMap", | ||||
|                 "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright", | ||||
|         layer: () => | ||||
|             AvailableBaseLayersImplementation.CreateBackgroundLayer( | ||||
|                 "osm", | ||||
|                 "OpenStreetMap", | ||||
|                 "https://tile.openstreetmap.org/{z}/{x}/{y}.png", | ||||
|                 "OpenStreetMap", | ||||
|                 "https://openStreetMap.org/copyright", | ||||
|                 19, | ||||
|                 false, false), | ||||
|                 false, | ||||
|                 false | ||||
|             ), | ||||
|         feature: null, | ||||
|         max_zoom: 19, | ||||
|         min_zoom: 0, | ||||
|         isBest: true, // Of course, OpenStreetMap is the best map!
 | ||||
|             category: "osmbasedmap" | ||||
|         category: "osmbasedmap", | ||||
|     } | ||||
| 
 | ||||
|     public readonly layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(AvailableBaseLayersImplementation.LoadProviderIndex()); | ||||
|     public readonly globalLayers = this.layerOverview.filter(layer => layer.feature?.geometry === undefined || layer.feature?.geometry === null) | ||||
|     public readonly localLayers = this.layerOverview.filter(layer => layer.feature?.geometry !== undefined && layer.feature?.geometry !== null) | ||||
|     public readonly layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat( | ||||
|         AvailableBaseLayersImplementation.LoadProviderIndex() | ||||
|     ) | ||||
|     public readonly globalLayers = this.layerOverview.filter( | ||||
|         (layer) => layer.feature?.geometry === undefined || layer.feature?.geometry === null | ||||
|     ) | ||||
|     public readonly localLayers = this.layerOverview.filter( | ||||
|         (layer) => layer.feature?.geometry !== undefined && layer.feature?.geometry !== null | ||||
|     ) | ||||
| 
 | ||||
|     private static LoadRasterIndex(): BaseLayer[] { | ||||
|         const layers: BaseLayer[] = [] | ||||
|         // @ts-ignore
 | ||||
|         const features = editorlayerindex.features; | ||||
|         const features = editorlayerindex.features | ||||
|         for (const i in features) { | ||||
|             const layer = features[i]; | ||||
|             const props = layer.properties; | ||||
|             const layer = features[i] | ||||
|             const props = layer.properties | ||||
| 
 | ||||
|             if (props.type === "bing") { | ||||
|                 // A lot of work to implement - see https://github.com/pietervdvn/MapComplete/issues/648
 | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (props.id === "MAPNIK") { | ||||
|                 // Already added by default
 | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (props.overlay) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (props.url.toLowerCase().indexOf("apikey") > 0) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (props.max_zoom < 19) { | ||||
|                 // We want users to zoom to level 19 when adding a point
 | ||||
|                 // If they are on a layer which hasn't enough precision, they can not zoom far enough. This is confusing, so we don't use this layer
 | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (props.name === undefined) { | ||||
|  | @ -68,8 +79,8 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL | |||
|                 continue | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             const leafletLayer: () => TileLayer = () => AvailableBaseLayersImplementation.CreateBackgroundLayer( | ||||
|             const leafletLayer: () => TileLayer = () => | ||||
|                 AvailableBaseLayersImplementation.CreateBackgroundLayer( | ||||
|                     props.id, | ||||
|                     props.name, | ||||
|                     props.url, | ||||
|  | @ -89,34 +100,35 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL | |||
|                 layer: leafletLayer, | ||||
|                 feature: layer.geometry !== null ? layer : null, | ||||
|                 isBest: props.best ?? false, | ||||
|                 category: props.category | ||||
|             }); | ||||
|                 category: props.category, | ||||
|             }) | ||||
|         } | ||||
|         return layers; | ||||
|         return layers | ||||
|     } | ||||
| 
 | ||||
|     private static LoadProviderIndex(): BaseLayer[] { | ||||
|         // @ts-ignore
 | ||||
|         X; // Import X to make sure the namespace is not optimized away
 | ||||
|         X // Import X to make sure the namespace is not optimized away
 | ||||
|         function l(id: string, name: string): BaseLayer { | ||||
|             try { | ||||
|                 const layer: any = L.tileLayer.provider(id, undefined); | ||||
|                 const layer: any = L.tileLayer.provider(id, undefined) | ||||
|                 return { | ||||
|                     feature: null, | ||||
|                     id: id, | ||||
|                     name: name, | ||||
|                     layer: () => L.tileLayer.provider(id, { | ||||
|                     layer: () => | ||||
|                         L.tileLayer.provider(id, { | ||||
|                             maxNativeZoom: layer.options?.maxZoom, | ||||
|                        maxZoom: Math.max(layer.options?.maxZoom ?? 19, 21) | ||||
|                             maxZoom: Math.max(layer.options?.maxZoom ?? 19, 21), | ||||
|                         }), | ||||
|                     min_zoom: 1, | ||||
|                     max_zoom: layer.options.maxZoom, | ||||
|                     category: "osmbasedmap", | ||||
|                     isBest: false | ||||
|                     isBest: false, | ||||
|                 } | ||||
|             } catch (e) { | ||||
|                 console.error("Could not find provided layer", name, e); | ||||
|                 return null; | ||||
|                 console.error("Could not find provided layer", name, e) | ||||
|                 return null | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -129,38 +141,50 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL | |||
|             l("CartoDB.PositronNoLabels", "Positron  - no labels (by CartoDB)"), | ||||
|             l("CartoDB.Voyager", "Voyager (by CartoDB)"), | ||||
|             l("CartoDB.VoyagerNoLabels", "Voyager  - no labels (by CartoDB)"), | ||||
|         ]; | ||||
|         return Utils.NoNull(layers); | ||||
| 
 | ||||
|         ] | ||||
|         return Utils.NoNull(layers) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Converts a layer from the editor-layer-index into a tilelayer usable by leaflet | ||||
|      */ | ||||
|     private static CreateBackgroundLayer(id: string, name: string, url: string, attribution: string, attributionUrl: string, | ||||
|                                          maxZoom: number, isWms: boolean, isWMTS?: boolean): TileLayer { | ||||
| 
 | ||||
|         url = url.replace("{zoom}", "{z}") | ||||
|             .replace("&BBOX={bbox}", "") | ||||
|             .replace("&bbox={bbox}", ""); | ||||
|     private static CreateBackgroundLayer( | ||||
|         id: string, | ||||
|         name: string, | ||||
|         url: string, | ||||
|         attribution: string, | ||||
|         attributionUrl: string, | ||||
|         maxZoom: number, | ||||
|         isWms: boolean, | ||||
|         isWMTS?: boolean | ||||
|     ): TileLayer { | ||||
|         url = url.replace("{zoom}", "{z}").replace("&BBOX={bbox}", "").replace("&bbox={bbox}", "") | ||||
| 
 | ||||
|         const subdomainsMatch = url.match(/{switch:[^}]*}/) | ||||
|         let domains: string[] = []; | ||||
|         let domains: string[] = [] | ||||
|         if (subdomainsMatch !== null) { | ||||
|             let domainsStr = subdomainsMatch[0].substr("{switch:".length); | ||||
|             domainsStr = domainsStr.substr(0, domainsStr.length - 1); | ||||
|             domains = domainsStr.split(","); | ||||
|             let domainsStr = subdomainsMatch[0].substr("{switch:".length) | ||||
|             domainsStr = domainsStr.substr(0, domainsStr.length - 1) | ||||
|             domains = domainsStr.split(",") | ||||
|             url = url.replace(/{switch:[^}]*}/, "{s}") | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         if (isWms) { | ||||
|             url = url.replace("&SRS={proj}", ""); | ||||
|             url = url.replace("&srs={proj}", ""); | ||||
|             const paramaters = ["format", "layers", "version", "service", "request", "styles", "transparent", "version"]; | ||||
|             const urlObj = new URL(url); | ||||
|             url = url.replace("&SRS={proj}", "") | ||||
|             url = url.replace("&srs={proj}", "") | ||||
|             const paramaters = [ | ||||
|                 "format", | ||||
|                 "layers", | ||||
|                 "version", | ||||
|                 "service", | ||||
|                 "request", | ||||
|                 "styles", | ||||
|                 "transparent", | ||||
|                 "version", | ||||
|             ] | ||||
|             const urlObj = new URL(url) | ||||
| 
 | ||||
|             const isUpper = urlObj.searchParams["LAYERS"] !== null; | ||||
|             const isUpper = urlObj.searchParams["LAYERS"] !== null | ||||
|             const options = { | ||||
|                 maxZoom: Math.max(maxZoom ?? 19, 21), | ||||
|                 maxNativeZoom: maxZoom ?? 19, | ||||
|  | @ -168,116 +192,117 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL | |||
|                 subdomains: domains, | ||||
|                 uppercase: isUpper, | ||||
|                 transparent: false, | ||||
|             }; | ||||
|             } | ||||
| 
 | ||||
|             for (const paramater of paramaters) { | ||||
|                 let p = paramater; | ||||
|                 let p = paramater | ||||
|                 if (isUpper) { | ||||
|                     p = paramater.toUpperCase(); | ||||
|                     p = paramater.toUpperCase() | ||||
|                 } | ||||
|                 options[paramater] = urlObj.searchParams.get(p); | ||||
|                 options[paramater] = urlObj.searchParams.get(p) | ||||
|             } | ||||
| 
 | ||||
|             if (options.transparent === null) { | ||||
|                 options.transparent = false; | ||||
|                 options.transparent = false | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             return L.tileLayer.wms(urlObj.protocol + "//" + urlObj.host + urlObj.pathname, options); | ||||
|             return L.tileLayer.wms(urlObj.protocol + "//" + urlObj.host + urlObj.pathname, options) | ||||
|         } | ||||
| 
 | ||||
|         if (attributionUrl) { | ||||
|             attribution = `<a href='${attributionUrl}' target='_blank'>${attribution}</a>`; | ||||
|             attribution = `<a href='${attributionUrl}' target='_blank'>${attribution}</a>` | ||||
|         } | ||||
| 
 | ||||
|         return L.tileLayer(url, | ||||
|             { | ||||
|         return L.tileLayer(url, { | ||||
|             attribution: attribution, | ||||
|             maxZoom: Math.max(21, maxZoom ?? 19), | ||||
|             maxNativeZoom: maxZoom ?? 19, | ||||
|             minZoom: 1, | ||||
|             // @ts-ignore
 | ||||
|             wmts: isWMTS ?? false, | ||||
|                 subdomains: domains | ||||
|             }); | ||||
|             subdomains: domains, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> { | ||||
|         return Stores.ListStabilized(location.map( | ||||
|             (currentLocation) => { | ||||
|         return Stores.ListStabilized( | ||||
|             location.map((currentLocation) => { | ||||
|                 if (currentLocation === undefined) { | ||||
|                     return this.layerOverview; | ||||
|                     return this.layerOverview | ||||
|                 } | ||||
|                 return this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat); | ||||
|             })); | ||||
|                 return this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat) | ||||
|             }) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     public SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: Store<string | string[]>): Store<BaseLayer> { | ||||
|         return this.AvailableLayersAt(location) | ||||
|             .map(available => { | ||||
|     public SelectBestLayerAccordingTo( | ||||
|         location: Store<Loc>, | ||||
|         preferedCategory: Store<string | string[]> | ||||
|     ): Store<BaseLayer> { | ||||
|         return this.AvailableLayersAt(location).map( | ||||
|             (available) => { | ||||
|                 // First float all 'best layers' to the top
 | ||||
|                 available.sort((a, b) => { | ||||
|                     if (a.isBest && b.isBest) { | ||||
|                             return 0; | ||||
|                         return 0 | ||||
|                     } | ||||
|                     if (!a.isBest) { | ||||
|                         return 1 | ||||
|                     } | ||||
| 
 | ||||
|                         return -1; | ||||
|                     } | ||||
|                 ) | ||||
|                     return -1 | ||||
|                 }) | ||||
| 
 | ||||
|                 if (preferedCategory.data === undefined) { | ||||
|                     return available[0] | ||||
|                 } | ||||
| 
 | ||||
|                 let prefered: string [] | ||||
|                 let prefered: string[] | ||||
|                 if (typeof preferedCategory.data === "string") { | ||||
|                     prefered = [preferedCategory.data] | ||||
|                 } else { | ||||
|                     prefered = preferedCategory.data; | ||||
|                     prefered = preferedCategory.data | ||||
|                 } | ||||
| 
 | ||||
|                 prefered.reverse(/*New list, inplace reverse is fine*/); | ||||
|                 prefered.reverse(/*New list, inplace reverse is fine*/) | ||||
|                 for (const category of prefered) { | ||||
|                     //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
 | ||||
|                     available.sort((a, b) => { | ||||
|                         if (a.category === category && b.category === category) { | ||||
|                                 return 0; | ||||
|                             return 0 | ||||
|                         } | ||||
|                         if (a.category !== category) { | ||||
|                             return 1 | ||||
|                         } | ||||
| 
 | ||||
|                             return -1; | ||||
|                         } | ||||
|                     ) | ||||
|                         return -1 | ||||
|                     }) | ||||
|                 } | ||||
|                 return available[0] | ||||
|             }, [preferedCategory]) | ||||
|             }, | ||||
|             [preferedCategory] | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] { | ||||
|         const availableLayers = [this.osmCarto] | ||||
|         if (lon === undefined || lat === undefined) { | ||||
|             return availableLayers.concat(this.globalLayers); | ||||
|             return availableLayers.concat(this.globalLayers) | ||||
|         } | ||||
|         const lonlat : [number, number] = [lon, lat]; | ||||
|         const lonlat: [number, number] = [lon, lat] | ||||
|         for (const layerOverviewItem of this.localLayers) { | ||||
|             const layer = layerOverviewItem; | ||||
|             const layer = layerOverviewItem | ||||
|             const bbox = BBox.get(layer.feature) | ||||
| 
 | ||||
|             if(!bbox.contains(lonlat)){ | ||||
|             if (!bbox.contains(lonlat)) { | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (GeoOperations.inside(lonlat, layer.feature)) { | ||||
|                 availableLayers.push(layer); | ||||
|                 availableLayers.push(layer) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return availableLayers.concat(this.globalLayers); | ||||
|         return availableLayers.concat(this.globalLayers) | ||||
|     } | ||||
| } | ||||
|  | @ -1,50 +1,49 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import AvailableBaseLayers from "./AvailableBaseLayers"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import BaseLayer from "../../Models/BaseLayer" | ||||
| import AvailableBaseLayers from "./AvailableBaseLayers" | ||||
| import Loc from "../../Models/Loc" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| /** | ||||
|  * Sets the current background layer to a layer that is actually available | ||||
|  */ | ||||
| export default class BackgroundLayerResetter { | ||||
| 
 | ||||
|     constructor(currentBackgroundLayer: UIEventSource<BaseLayer>, | ||||
|     constructor( | ||||
|         currentBackgroundLayer: UIEventSource<BaseLayer>, | ||||
|         location: UIEventSource<Loc>, | ||||
|         availableLayers: UIEventSource<BaseLayer[]>, | ||||
|                 defaultLayerId: string = undefined) { | ||||
| 
 | ||||
|         defaultLayerId: string = undefined | ||||
|     ) { | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         defaultLayerId = defaultLayerId ?? AvailableBaseLayers.osmCarto.id; | ||||
|         defaultLayerId = defaultLayerId ?? AvailableBaseLayers.osmCarto.id | ||||
| 
 | ||||
|         // Change the baselayer back to OSM if we go out of the current range of the layer
 | ||||
|         availableLayers.addCallbackAndRun(availableLayers => { | ||||
|             let defaultLayer = undefined; | ||||
|             const currentLayer = currentBackgroundLayer.data.id; | ||||
|         availableLayers.addCallbackAndRun((availableLayers) => { | ||||
|             let defaultLayer = undefined | ||||
|             const currentLayer = currentBackgroundLayer.data.id | ||||
|             for (const availableLayer of availableLayers) { | ||||
|                 if (availableLayer.id === currentLayer) { | ||||
| 
 | ||||
|                     if (availableLayer.max_zoom < location.data.zoom) { | ||||
|                         break; | ||||
|                         break | ||||
|                     } | ||||
| 
 | ||||
|                     if (availableLayer.min_zoom > location.data.zoom) { | ||||
|                         break; | ||||
|                         break | ||||
|                     } | ||||
|                     if (availableLayer.id === defaultLayerId) { | ||||
|                         defaultLayer = availableLayer; | ||||
|                         defaultLayer = availableLayer | ||||
|                     } | ||||
|                     return; // All good - the current layer still works!
 | ||||
|                     return // All good - the current layer still works!
 | ||||
|                 } | ||||
|             } | ||||
|             // Oops, we panned out of range for this layer!
 | ||||
|             console.log("AvailableBaseLayers-actor: detected that the current bounds aren't sufficient anymore - reverting to OSM standard") | ||||
|             currentBackgroundLayer.setData(defaultLayer ?? AvailableBaseLayers.osmCarto); | ||||
|         }); | ||||
| 
 | ||||
|             console.log( | ||||
|                 "AvailableBaseLayers-actor: detected that the current bounds aren't sufficient anymore - reverting to OSM standard" | ||||
|             ) | ||||
|             currentBackgroundLayer.setData(defaultLayer ?? AvailableBaseLayers.osmCarto) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,35 +1,33 @@ | |||
| import {ElementStorage} from "../ElementStorage"; | ||||
| import {Changes} from "../Osm/Changes"; | ||||
| import { ElementStorage } from "../ElementStorage" | ||||
| import { Changes } from "../Osm/Changes" | ||||
| 
 | ||||
| export default class ChangeToElementsActor { | ||||
|     constructor(changes: Changes, allElements: ElementStorage) { | ||||
|         changes.pendingChanges.addCallbackAndRun(changes => { | ||||
|         changes.pendingChanges.addCallbackAndRun((changes) => { | ||||
|             for (const change of changes) { | ||||
|                 const id = change.type + "/" + change.id; | ||||
|                 const id = change.type + "/" + change.id | ||||
|                 if (!allElements.has(id)) { | ||||
|                     continue; // Ignored as the geometryFixer will introduce this
 | ||||
|                     continue // Ignored as the geometryFixer will introduce this
 | ||||
|                 } | ||||
|                 const src = allElements.getEventSourceById(id) | ||||
| 
 | ||||
|                 let changed = false; | ||||
|                 let changed = false | ||||
|                 for (const kv of change.tags ?? []) { | ||||
|                     // Apply tag changes and ping the consumers
 | ||||
|                     const k = kv.k | ||||
|                     let v = kv.v | ||||
|                     if (v === "") { | ||||
|                         v = undefined; | ||||
|                         v = undefined | ||||
|                     } | ||||
|                     if (src.data[k] === v) { | ||||
|                         continue | ||||
|                     } | ||||
|                     changed = true; | ||||
|                     src.data[k] = v; | ||||
|                     changed = true | ||||
|                     src.data[k] = v | ||||
|                 } | ||||
|                 if (changed) { | ||||
|                     src.ping() | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  |  | |||
|  | @ -1,60 +1,59 @@ | |||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import Svg from "../../Svg"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| import {VariableUiElement} from "../../UI/Base/VariableUIElement"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {QueryParameters} from "../Web/QueryParameters"; | ||||
| import {BBox} from "../BBox"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"; | ||||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import Svg from "../../Svg" | ||||
| import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||
| import { VariableUiElement } from "../../UI/Base/VariableUIElement" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import { QueryParameters } from "../Web/QueryParameters" | ||||
| import { BBox } from "../BBox" | ||||
| import Constants from "../../Models/Constants" | ||||
| import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource" | ||||
| 
 | ||||
| export interface GeoLocationPointProperties { | ||||
|     id: "gps", | ||||
|     "user:location": "yes", | ||||
|     "date": string, | ||||
|     "latitude": number | ||||
|     "longitude": number, | ||||
|     "speed": number, | ||||
|     "accuracy": number | ||||
|     "heading": number | ||||
|     "altitude": number | ||||
|     id: "gps" | ||||
|     "user:location": "yes" | ||||
|     date: string | ||||
|     latitude: number | ||||
|     longitude: number | ||||
|     speed: number | ||||
|     accuracy: number | ||||
|     heading: number | ||||
|     altitude: number | ||||
| } | ||||
| 
 | ||||
| export default class GeoLocationHandler extends VariableUiElement { | ||||
| 
 | ||||
|     private readonly currentLocation?: SimpleFeatureSource | ||||
| 
 | ||||
|     /** | ||||
|      * Wether or not the geolocation is active, aka the user requested the current location | ||||
|      */ | ||||
|     private readonly _isActive: UIEventSource<boolean>; | ||||
|     private readonly _isActive: UIEventSource<boolean> | ||||
| 
 | ||||
|     /** | ||||
|      * Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user | ||||
|      */ | ||||
|     private readonly _isLocked: UIEventSource<boolean>; | ||||
|     private readonly _isLocked: UIEventSource<boolean> | ||||
| 
 | ||||
|     /** | ||||
|      * The callback over the permission API | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _permission: UIEventSource<string>; | ||||
|     private readonly _permission: UIEventSource<string> | ||||
|     /** | ||||
|      * Literally: _currentGPSLocation.data != undefined | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _hasLocation: Store<boolean>; | ||||
|     private readonly _currentGPSLocation: UIEventSource<GeolocationCoordinates>; | ||||
|     private readonly _hasLocation: Store<boolean> | ||||
|     private readonly _currentGPSLocation: UIEventSource<GeolocationCoordinates> | ||||
|     /** | ||||
|      * Kept in order to update the marker | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _leafletMap: UIEventSource<L.Map>; | ||||
|     private readonly _leafletMap: UIEventSource<L.Map> | ||||
| 
 | ||||
|     /** | ||||
|      * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs | ||||
|      */ | ||||
|     private _lastUserRequest: UIEventSource<Date>; | ||||
|     private _lastUserRequest: UIEventSource<Date> | ||||
| 
 | ||||
|     /** | ||||
|      * A small flag on localstorage. If the user previously granted the geolocation, it will be set. | ||||
|  | @ -64,54 +63,52 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|      * If the user denies the geolocation this time, we unset this flag | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _previousLocationGrant: UIEventSource<string>; | ||||
|     private readonly _layoutToUse: LayoutConfig; | ||||
|     private readonly _previousLocationGrant: UIEventSource<string> | ||||
|     private readonly _layoutToUse: LayoutConfig | ||||
| 
 | ||||
|     constructor( | ||||
|         state: { | ||||
|             selectedElement: UIEventSource<any>; | ||||
|             currentUserLocation?: SimpleFeatureSource, | ||||
|             leafletMap: UIEventSource<any>, | ||||
|             layoutToUse: LayoutConfig, | ||||
|     constructor(state: { | ||||
|         selectedElement: UIEventSource<any> | ||||
|         currentUserLocation?: SimpleFeatureSource | ||||
|         leafletMap: UIEventSource<any> | ||||
|         layoutToUse: LayoutConfig | ||||
|         featureSwitchGeolocation: UIEventSource<boolean> | ||||
|         } | ||||
|     ) { | ||||
|         const currentGPSLocation = new UIEventSource<GeolocationCoordinates>(undefined, "GPS-coordinate") | ||||
|     }) { | ||||
|         const currentGPSLocation = new UIEventSource<GeolocationCoordinates>( | ||||
|             undefined, | ||||
|             "GPS-coordinate" | ||||
|         ) | ||||
|         const leafletMap = state.leafletMap | ||||
|         const initedAt = new Date() | ||||
|         let autozoomDone = false; | ||||
|         const hasLocation = currentGPSLocation.map( | ||||
|             (location) => location !== undefined | ||||
|         ); | ||||
|         const previousLocationGrant = LocalStorageSource.Get( | ||||
|             "geolocation-permissions" | ||||
|         ); | ||||
|         const isActive = new UIEventSource<boolean>(false); | ||||
|         const isLocked = new UIEventSource<boolean>(false); | ||||
|         const permission = new UIEventSource<string>(""); | ||||
|         const lastClick = new UIEventSource<Date>(undefined); | ||||
|         const lastClickWithinThreeSecs = lastClick.map(lastClick => { | ||||
|         let autozoomDone = false | ||||
|         const hasLocation = currentGPSLocation.map((location) => location !== undefined) | ||||
|         const previousLocationGrant = LocalStorageSource.Get("geolocation-permissions") | ||||
|         const isActive = new UIEventSource<boolean>(false) | ||||
|         const isLocked = new UIEventSource<boolean>(false) | ||||
|         const permission = new UIEventSource<string>("") | ||||
|         const lastClick = new UIEventSource<Date>(undefined) | ||||
|         const lastClickWithinThreeSecs = lastClick.map((lastClick) => { | ||||
|             if (lastClick === undefined) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
|             const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000 | ||||
|             return timeDiff <= 3 | ||||
|         }) | ||||
| 
 | ||||
|         const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon") | ||||
|         const willFocus = lastClick.map(lastUserRequest => { | ||||
|         const latLonGiven = | ||||
|             QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon") | ||||
|         const willFocus = lastClick.map((lastUserRequest) => { | ||||
|             const timeDiffInited = (new Date().getTime() - initedAt.getTime()) / 1000 | ||||
|             if (!latLonGiven && !autozoomDone && timeDiffInited < Constants.zoomToLocationTimeout) { | ||||
|                 return true | ||||
|             } | ||||
|             if (lastUserRequest === undefined) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
|             const timeDiff = (new Date().getTime() - lastUserRequest.getTime()) / 1000 | ||||
|             return timeDiff <= Constants.zoomToLocationTimeout | ||||
|         }) | ||||
| 
 | ||||
|         lastClick.addCallbackAndRunD(_ => { | ||||
|         lastClick.addCallbackAndRunD((_) => { | ||||
|             window.setTimeout(() => { | ||||
|                 if (lastClickWithinThreeSecs.data || willFocus.data) { | ||||
|                     lastClick.ping() | ||||
|  | @ -123,7 +120,7 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|             hasLocation.map( | ||||
|                 (hasLocationData) => { | ||||
|                     if (permission.data === "denied") { | ||||
|                         return Svg.location_refused_svg(); | ||||
|                         return Svg.location_refused_svg() | ||||
|                     } | ||||
| 
 | ||||
|                     if (!isActive.data) { | ||||
|  | @ -134,7 +131,7 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|                         // If will focus is active too, we indicate this differently
 | ||||
|                         const icon = willFocus.data ? Svg.location_svg() : Svg.location_empty_svg() | ||||
|                         icon.SetStyle("animation: spin 4s linear infinite;") | ||||
|                         return icon; | ||||
|                         return icon | ||||
|                     } | ||||
|                     if (isLocked.data) { | ||||
|                         return Svg.location_locked_svg() | ||||
|  | @ -144,38 +141,37 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|                     } | ||||
| 
 | ||||
|                     // We have a location, so we show a dot in the center
 | ||||
|                     return Svg.location_svg(); | ||||
|                     return Svg.location_svg() | ||||
|                 }, | ||||
|                 [isActive, isLocked, permission, lastClickWithinThreeSecs, willFocus] | ||||
|             ) | ||||
|         ); | ||||
|         ) | ||||
|         this.SetClass("mapcontrol") | ||||
|         this._isActive = isActive; | ||||
|         this._isLocked = isLocked; | ||||
|         this._isActive = isActive | ||||
|         this._isLocked = isLocked | ||||
|         this._permission = permission | ||||
|         this._previousLocationGrant = previousLocationGrant; | ||||
|         this._currentGPSLocation = currentGPSLocation; | ||||
|         this._leafletMap = leafletMap; | ||||
|         this._layoutToUse = state.layoutToUse; | ||||
|         this._hasLocation = hasLocation; | ||||
|         this._previousLocationGrant = previousLocationGrant | ||||
|         this._currentGPSLocation = currentGPSLocation | ||||
|         this._leafletMap = leafletMap | ||||
|         this._layoutToUse = state.layoutToUse | ||||
|         this._hasLocation = hasLocation | ||||
|         this._lastUserRequest = lastClick | ||||
|         const self = this; | ||||
|         const self = this | ||||
| 
 | ||||
|         const currentPointer = this._isActive.map( | ||||
|             (isActive) => { | ||||
|                 if (isActive && !self._hasLocation.data) { | ||||
|                     return "cursor-wait"; | ||||
|                     return "cursor-wait" | ||||
|                 } | ||||
|                 return "cursor-pointer"; | ||||
|                 return "cursor-pointer" | ||||
|             }, | ||||
|             [this._hasLocation] | ||||
|         ); | ||||
|         ) | ||||
|         currentPointer.addCallbackAndRun((pointerClass) => { | ||||
|             self.RemoveClass("cursor-wait") | ||||
|             self.RemoveClass("cursor-pointer") | ||||
|             self.SetClass(pointerClass); | ||||
|         }); | ||||
| 
 | ||||
|             self.SetClass(pointerClass) | ||||
|         }) | ||||
| 
 | ||||
|         this.onClick(() => { | ||||
|             /* | ||||
|  | @ -197,14 +193,16 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             self.init(true, true); | ||||
|         }); | ||||
|             self.init(true, true) | ||||
|         }) | ||||
| 
 | ||||
|         const doAutoZoomToLocation = | ||||
|             !latLonGiven && | ||||
|             state.featureSwitchGeolocation.data && | ||||
|             state.selectedElement.data !== undefined | ||||
|         this.init(false, doAutoZoomToLocation) | ||||
| 
 | ||||
|         const doAutoZoomToLocation = !latLonGiven && state.featureSwitchGeolocation.data && state.selectedElement.data !== undefined | ||||
|         this.init(false, doAutoZoomToLocation); | ||||
| 
 | ||||
|         isLocked.addCallbackAndRunD(isLocked => { | ||||
|         isLocked.addCallbackAndRunD((isLocked) => { | ||||
|             if (isLocked) { | ||||
|                 leafletMap.data?.dragging?.disable() | ||||
|             } else { | ||||
|  | @ -214,47 +212,45 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
| 
 | ||||
|         this.currentLocation = state.currentUserLocation | ||||
|         this._currentGPSLocation.addCallback((location) => { | ||||
|             self._previousLocationGrant.setData("granted"); | ||||
|             self._previousLocationGrant.setData("granted") | ||||
|             const feature = { | ||||
|                 "type": "Feature", | ||||
|                 type: "Feature", | ||||
|                 properties: <GeoLocationPointProperties>{ | ||||
|                     id: "gps", | ||||
|                     "user:location": "yes", | ||||
|                     "date": new Date().toISOString(), | ||||
|                     "latitude": location.latitude, | ||||
|                     "longitude": location.longitude, | ||||
|                     "speed": location.speed, | ||||
|                     "accuracy": location.accuracy, | ||||
|                     "heading": location.heading, | ||||
|                     "altitude": location.altitude | ||||
|                     date: new Date().toISOString(), | ||||
|                     latitude: location.latitude, | ||||
|                     longitude: location.longitude, | ||||
|                     speed: location.speed, | ||||
|                     accuracy: location.accuracy, | ||||
|                     heading: location.heading, | ||||
|                     altitude: location.altitude, | ||||
|                 }, | ||||
|                 geometry: { | ||||
|                     type: "Point", | ||||
|                     coordinates: [location.longitude, location.latitude], | ||||
|                 } | ||||
|                 }, | ||||
|             } | ||||
| 
 | ||||
|             self.currentLocation?.features?.setData([{feature, freshness: new Date()}]) | ||||
|             self.currentLocation?.features?.setData([{ feature, freshness: new Date() }]) | ||||
| 
 | ||||
|             if (willFocus.data) { | ||||
|                 console.log("Zooming to user location: willFocus is set") | ||||
|                 lastClick.setData(undefined); | ||||
|                 autozoomDone = true; | ||||
|                 self.MoveToCurrentLocation(16); | ||||
|                 lastClick.setData(undefined) | ||||
|                 autozoomDone = true | ||||
|                 self.MoveToCurrentLocation(16) | ||||
|             } else if (self._isLocked.data) { | ||||
|                 self.MoveToCurrentLocation(); | ||||
|                 self.MoveToCurrentLocation() | ||||
|             } | ||||
| 
 | ||||
|         }); | ||||
| 
 | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private init(askPermission: boolean, zoomToLocation: boolean) { | ||||
|         const self = this; | ||||
|         const self = this | ||||
| 
 | ||||
|         if (self._isActive.data) { | ||||
|             self.MoveToCurrentLocation(16); | ||||
|             return; | ||||
|             self.MoveToCurrentLocation(16) | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (typeof navigator === "undefined") { | ||||
|  | @ -262,27 +258,25 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             navigator?.permissions | ||||
|                 ?.query({name: "geolocation"}) | ||||
|                 ?.then(function (status) { | ||||
|                     console.log("Geolocation permission is ", status.state); | ||||
|             navigator?.permissions?.query({ name: "geolocation" })?.then(function (status) { | ||||
|                 console.log("Geolocation permission is ", status.state) | ||||
|                 if (status.state === "granted") { | ||||
|                         self.StartGeolocating(zoomToLocation); | ||||
|                     self.StartGeolocating(zoomToLocation) | ||||
|                 } | ||||
|                     self._permission.setData(status.state); | ||||
|                 self._permission.setData(status.state) | ||||
|                 status.onchange = function () { | ||||
|                         self._permission.setData(status.state); | ||||
|                     }; | ||||
|                 }); | ||||
|                     self._permission.setData(status.state) | ||||
|                 } | ||||
|             }) | ||||
|         } catch (e) { | ||||
|             console.error(e); | ||||
|             console.error(e) | ||||
|         } | ||||
| 
 | ||||
|         if (askPermission) { | ||||
|             self.StartGeolocating(zoomToLocation); | ||||
|             self.StartGeolocating(zoomToLocation) | ||||
|         } else if (this._previousLocationGrant.data === "granted") { | ||||
|             this._previousLocationGrant.setData(""); | ||||
|             self.StartGeolocating(zoomToLocation); | ||||
|             this._previousLocationGrant.setData("") | ||||
|             self.StartGeolocating(zoomToLocation) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -337,20 +331,20 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|      * resultingLocation // => [51.3, 4.1]
 | ||||
|      */ | ||||
|     private MoveToCurrentLocation(targetZoom?: number) { | ||||
|         const location = this._currentGPSLocation.data; | ||||
|         this._lastUserRequest.setData(undefined); | ||||
|         const location = this._currentGPSLocation.data | ||||
|         this._lastUserRequest.setData(undefined) | ||||
| 
 | ||||
|         if ( | ||||
|             this._currentGPSLocation.data.latitude === 0 && | ||||
|             this._currentGPSLocation.data.longitude === 0 | ||||
|         ) { | ||||
|             console.debug("Not moving to GPS-location: it is null island"); | ||||
|             return; | ||||
|             console.debug("Not moving to GPS-location: it is null island") | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         // We check that the GPS location is not out of bounds
 | ||||
|         const b = this._layoutToUse.lockLocation; | ||||
|         let inRange = true; | ||||
|         const b = this._layoutToUse.lockLocation | ||||
|         let inRange = true | ||||
|         if (b) { | ||||
|             if (b !== true) { | ||||
|                 // B is an array with our locklocation
 | ||||
|  | @ -358,41 +352,44 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|             } | ||||
|         } | ||||
|         if (!inRange) { | ||||
|             console.log("Not zooming to GPS location: out of bounds", b, location); | ||||
|             console.log("Not zooming to GPS location: out of bounds", b, location) | ||||
|         } else { | ||||
|             const currentZoom = this._leafletMap.data.getZoom() | ||||
|             this._leafletMap.data.setView([location.latitude, location.longitude], Math.max(targetZoom ?? 0, currentZoom)); | ||||
|             this._leafletMap.data.setView( | ||||
|                 [location.latitude, location.longitude], | ||||
|                 Math.max(targetZoom ?? 0, currentZoom) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private StartGeolocating(zoomToGPS = true) { | ||||
|         const self = this; | ||||
|         const self = this | ||||
| 
 | ||||
|         this._lastUserRequest.setData(zoomToGPS ? new Date() : new Date(0)) | ||||
|         if (self._permission.data === "denied") { | ||||
|             self._previousLocationGrant.setData(""); | ||||
|             self._previousLocationGrant.setData("") | ||||
|             self._isActive.setData(false) | ||||
|             return ""; | ||||
|             return "" | ||||
|         } | ||||
|         if (this._currentGPSLocation.data !== undefined) { | ||||
|             this.MoveToCurrentLocation(16); | ||||
|             this.MoveToCurrentLocation(16) | ||||
|         } | ||||
| 
 | ||||
|         if (self._isActive.data) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         self._isActive.setData(true); | ||||
|         self._isActive.setData(true) | ||||
| 
 | ||||
|         navigator.geolocation.watchPosition( | ||||
|             function (position) { | ||||
|                 self._currentGPSLocation.setData(position.coords); | ||||
|                 self._currentGPSLocation.setData(position.coords) | ||||
|             }, | ||||
|             function () { | ||||
|                 console.warn("Could not get location with navigator.geolocation"); | ||||
|                 console.warn("Could not get location with navigator.geolocation") | ||||
|             }, | ||||
|             { | ||||
|                 enableHighAccuracy: true | ||||
|                 enableHighAccuracy: true, | ||||
|             } | ||||
|         ); | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,112 +1,124 @@ | |||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import {Or} from "../Tags/Or"; | ||||
| import {Overpass} from "../Osm/Overpass"; | ||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {TagsFilter} from "../Tags/TagsFilter"; | ||||
| import SimpleMetaTagger from "../SimpleMetaTagger"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import RelationsTracker from "../Osm/RelationsTracker"; | ||||
| import {BBox} from "../BBox"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import TileFreshnessCalculator from "../FeatureSource/TileFreshnessCalculator"; | ||||
| import {Tiles} from "../../Models/TileRange"; | ||||
| 
 | ||||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import { Or } from "../Tags/Or" | ||||
| import { Overpass } from "../Osm/Overpass" | ||||
| import FeatureSource from "../FeatureSource/FeatureSource" | ||||
| import { Utils } from "../../Utils" | ||||
| import { TagsFilter } from "../Tags/TagsFilter" | ||||
| import SimpleMetaTagger from "../SimpleMetaTagger" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import RelationsTracker from "../Osm/RelationsTracker" | ||||
| import { BBox } from "../BBox" | ||||
| import Loc from "../../Models/Loc" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import Constants from "../../Models/Constants" | ||||
| import TileFreshnessCalculator from "../FeatureSource/TileFreshnessCalculator" | ||||
| import { Tiles } from "../../Models/TileRange" | ||||
| 
 | ||||
| export default class OverpassFeatureSource implements FeatureSource { | ||||
| 
 | ||||
|     public readonly name = "OverpassFeatureSource" | ||||
| 
 | ||||
|     /** | ||||
|      * The last loaded features of the geojson | ||||
|      */ | ||||
|     public readonly features: UIEventSource<{ feature: any, freshness: Date }[]> = new UIEventSource<any[]>(undefined); | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = | ||||
|         new UIEventSource<any[]>(undefined) | ||||
| 
 | ||||
|     public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
|     public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0) | ||||
| 
 | ||||
|     public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||
|     public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0); | ||||
|     public readonly relationsTracker: RelationsTracker | ||||
| 
 | ||||
|     public readonly relationsTracker: RelationsTracker; | ||||
| 
 | ||||
| 
 | ||||
|     private readonly retries: UIEventSource<number> = new UIEventSource<number>(0); | ||||
|     private readonly retries: UIEventSource<number> = new UIEventSource<number>(0) | ||||
| 
 | ||||
|     private readonly state: { | ||||
|         readonly locationControl: Store<Loc>, | ||||
|         readonly layoutToUse: LayoutConfig, | ||||
|         readonly overpassUrl: Store<string[]>; | ||||
|         readonly overpassTimeout: Store<number>; | ||||
|         readonly locationControl: Store<Loc> | ||||
|         readonly layoutToUse: LayoutConfig | ||||
|         readonly overpassUrl: Store<string[]> | ||||
|         readonly overpassTimeout: Store<number> | ||||
|         readonly currentBounds: Store<BBox> | ||||
|     } | ||||
|     private readonly _isActive: Store<boolean> | ||||
|     /** | ||||
|      * Callback to handle all the data | ||||
|      */ | ||||
|     private readonly onBboxLoaded: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void; | ||||
|     private readonly onBboxLoaded: ( | ||||
|         bbox: BBox, | ||||
|         date: Date, | ||||
|         layers: LayerConfig[], | ||||
|         zoomlevel: number | ||||
|     ) => void | ||||
| 
 | ||||
|     /** | ||||
|      * Keeps track of how fresh the data is | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly freshnesses: Map<string, TileFreshnessCalculator>; | ||||
|     private readonly freshnesses: Map<string, TileFreshnessCalculator> | ||||
| 
 | ||||
|     constructor( | ||||
|         state: { | ||||
|             readonly locationControl: Store<Loc>, | ||||
|             readonly layoutToUse: LayoutConfig, | ||||
|             readonly overpassUrl: Store<string[]>; | ||||
|             readonly overpassTimeout: Store<number>; | ||||
|             readonly overpassMaxZoom: Store<number>, | ||||
|             readonly locationControl: Store<Loc> | ||||
|             readonly layoutToUse: LayoutConfig | ||||
|             readonly overpassUrl: Store<string[]> | ||||
|             readonly overpassTimeout: Store<number> | ||||
|             readonly overpassMaxZoom: Store<number> | ||||
|             readonly currentBounds: Store<BBox> | ||||
|         }, | ||||
|         options: { | ||||
|             padToTiles: Store<number>, | ||||
|             isActive?: Store<boolean>, | ||||
|             relationTracker: RelationsTracker, | ||||
|             onBboxLoaded?: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void, | ||||
|             padToTiles: Store<number> | ||||
|             isActive?: Store<boolean> | ||||
|             relationTracker: RelationsTracker | ||||
|             onBboxLoaded?: ( | ||||
|                 bbox: BBox, | ||||
|                 date: Date, | ||||
|                 layers: LayerConfig[], | ||||
|                 zoomlevel: number | ||||
|             ) => void | ||||
|             freshnesses?: Map<string, TileFreshnessCalculator> | ||||
|         }) { | ||||
| 
 | ||||
|         } | ||||
|     ) { | ||||
|         this.state = state | ||||
|         this._isActive = options.isActive; | ||||
|         this._isActive = options.isActive | ||||
|         this.onBboxLoaded = options.onBboxLoaded | ||||
|         this.relationsTracker = options.relationTracker | ||||
|         this.freshnesses = options.freshnesses | ||||
|         const self = this; | ||||
|         state.currentBounds.addCallback(_ => { | ||||
|         const self = this | ||||
|         state.currentBounds.addCallback((_) => { | ||||
|             self.update(options.padToTiles.data) | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private GetFilter(interpreterUrl: string, layersToDownload: LayerConfig[]): Overpass { | ||||
|         let filters: TagsFilter[] = []; | ||||
|         let extraScripts: string[] = []; | ||||
|         let filters: TagsFilter[] = [] | ||||
|         let extraScripts: string[] = [] | ||||
|         for (const layer of layersToDownload) { | ||||
|             if (layer.source.overpassScript !== undefined) { | ||||
|                 extraScripts.push(layer.source.overpassScript) | ||||
|             } else { | ||||
|                 filters.push(layer.source.osmTags); | ||||
|                 filters.push(layer.source.osmTags) | ||||
|             } | ||||
|         } | ||||
|         filters = Utils.NoNull(filters) | ||||
|         extraScripts = Utils.NoNull(extraScripts) | ||||
|         if (filters.length + extraScripts.length === 0) { | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
|         return new Overpass(new Or(filters), extraScripts, interpreterUrl, this.state.overpassTimeout, this.relationsTracker); | ||||
|         return new Overpass( | ||||
|             new Or(filters), | ||||
|             extraScripts, | ||||
|             interpreterUrl, | ||||
|             this.state.overpassTimeout, | ||||
|             this.relationsTracker | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private update(paddedZoomLevel: number) { | ||||
|         if (!this._isActive.data) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         const self = this; | ||||
|         this.updateAsync(paddedZoomLevel).then(bboxDate => { | ||||
|         const self = this | ||||
|         this.updateAsync(paddedZoomLevel).then((bboxDate) => { | ||||
|             if (bboxDate === undefined || self.onBboxLoaded === undefined) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             const [bbox, date, layers] = bboxDate | ||||
|             self.onBboxLoaded(bbox, date, layers, paddedZoomLevel) | ||||
|  | @ -115,56 +127,58 @@ export default class OverpassFeatureSource implements FeatureSource { | |||
| 
 | ||||
|     private async updateAsync(padToZoomLevel: number): Promise<[BBox, Date, LayerConfig[]]> { | ||||
|         if (this.runningQuery.data) { | ||||
|             console.log("Still running a query, not updating"); | ||||
|             return undefined; | ||||
|             console.log("Still running a query, not updating") | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         if (this.timeout.data > 0) { | ||||
|             console.log("Still in timeout - not updating") | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         let data: any = undefined | ||||
|         let date: Date = undefined | ||||
|         let lastUsed = 0; | ||||
| 
 | ||||
|         let lastUsed = 0 | ||||
| 
 | ||||
|         const layersToDownload = [] | ||||
|         const neededTiles = this.state.currentBounds.data.expandToTileBounds(padToZoomLevel).containingTileRange(padToZoomLevel) | ||||
|         const neededTiles = this.state.currentBounds.data | ||||
|             .expandToTileBounds(padToZoomLevel) | ||||
|             .containingTileRange(padToZoomLevel) | ||||
|         for (const layer of this.state.layoutToUse.layers) { | ||||
| 
 | ||||
|             if (typeof (layer) === "string") { | ||||
|             if (typeof layer === "string") { | ||||
|                 throw "A layer was not expanded!" | ||||
|             } | ||||
|             if (Constants.priviliged_layers.indexOf(layer.id) >= 0) { | ||||
|                 continue | ||||
|             } | ||||
|             if (this.state.locationControl.data.zoom < layer.minzoom) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
|             if (layer.doNotDownload) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
|             if (layer.source.geojsonSource !== undefined) { | ||||
|                 // Not our responsibility to download this layer!
 | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
|             const freshness = this.freshnesses?.get(layer.id) | ||||
|             if (freshness !== undefined) { | ||||
|                 const oldestDataDate = Math.min(...Tiles.MapRange(neededTiles, (x, y) => { | ||||
|                     const date = freshness.freshnessFor(padToZoomLevel, x, y); | ||||
|                 const oldestDataDate = | ||||
|                     Math.min( | ||||
|                         ...Tiles.MapRange(neededTiles, (x, y) => { | ||||
|                             const date = freshness.freshnessFor(padToZoomLevel, x, y) | ||||
|                             if (date === undefined) { | ||||
|                                 return 0 | ||||
|                             } | ||||
|                             return date.getTime() | ||||
|                 })) / 1000; | ||||
|                         }) | ||||
|                     ) / 1000 | ||||
|                 const now = new Date().getTime() | ||||
|                 const minRequiredAge = (now / 1000) - layer.maxAgeOfCache | ||||
|                 const minRequiredAge = now / 1000 - layer.maxAgeOfCache | ||||
|                 if (oldestDataDate >= minRequiredAge) { | ||||
|                     // still fresh enough - not updating
 | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             layersToDownload.push(layer) | ||||
|  | @ -172,34 +186,35 @@ export default class OverpassFeatureSource implements FeatureSource { | |||
| 
 | ||||
|         if (layersToDownload.length == 0) { | ||||
|             console.debug("Not updating - no layers needed") | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         const self = this; | ||||
|         const self = this | ||||
|         const overpassUrls = self.state.overpassUrl.data | ||||
|         let bounds: BBox | ||||
|         do { | ||||
|             try { | ||||
| 
 | ||||
|                 bounds = this.state.currentBounds.data?.pad(this.state.layoutToUse.widenFactor)?.expandToTileBounds(padToZoomLevel); | ||||
|                 bounds = this.state.currentBounds.data | ||||
|                     ?.pad(this.state.layoutToUse.widenFactor) | ||||
|                     ?.expandToTileBounds(padToZoomLevel) | ||||
| 
 | ||||
|                 if (bounds === undefined) { | ||||
|                     return undefined; | ||||
|                     return undefined | ||||
|                 } | ||||
| 
 | ||||
|                 const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload); | ||||
|                 const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload) | ||||
| 
 | ||||
|                 if (overpass === undefined) { | ||||
|                     return undefined; | ||||
|                     return undefined | ||||
|                 } | ||||
|                 this.runningQuery.setData(true); | ||||
|                 this.runningQuery.setData(true) | ||||
| 
 | ||||
|                 [data, date] = await overpass.queryGeoJson(bounds) | ||||
|                 ;[data, date] = await overpass.queryGeoJson(bounds) | ||||
|                 console.log("Querying overpass is done", data) | ||||
|             } catch (e) { | ||||
|                 self.retries.data++; | ||||
|                 self.retries.ping(); | ||||
|                 console.error(`QUERY FAILED due to`, e); | ||||
|                 self.retries.data++ | ||||
|                 self.retries.ping() | ||||
|                 console.error(`QUERY FAILED due to`, e) | ||||
| 
 | ||||
|                 await Utils.waitFor(1000) | ||||
| 
 | ||||
|  | @ -208,34 +223,38 @@ export default class OverpassFeatureSource implements FeatureSource { | |||
|                     console.log("Trying next time with", overpassUrls[lastUsed]) | ||||
|                 } else { | ||||
|                     lastUsed = 0 | ||||
|                     self.timeout.setData(self.retries.data * 5); | ||||
|                     self.timeout.setData(self.retries.data * 5) | ||||
| 
 | ||||
|                     while (self.timeout.data > 0) { | ||||
|                         await Utils.waitFor(1000) | ||||
|                         console.log(self.timeout.data) | ||||
|                         self.timeout.data-- | ||||
|                         self.timeout.ping(); | ||||
|                         self.timeout.ping() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } while (data === undefined && this._isActive.data); | ||||
| 
 | ||||
|         } while (data === undefined && this._isActive.data) | ||||
| 
 | ||||
|         try { | ||||
|             if (data === undefined) { | ||||
|                 return undefined | ||||
|             } | ||||
|             data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date, undefined, this.state)); | ||||
|             self.features.setData(data.features.map(f => ({feature: f, freshness: date}))); | ||||
|             return [bounds, date, layersToDownload]; | ||||
|             data.features.forEach((feature) => | ||||
|                 SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature( | ||||
|                     feature, | ||||
|                     date, | ||||
|                     undefined, | ||||
|                     this.state | ||||
|                 ) | ||||
|             ) | ||||
|             self.features.setData(data.features.map((f) => ({ feature: f, freshness: date }))) | ||||
|             return [bounds, date, layersToDownload] | ||||
|         } catch (e) { | ||||
|             console.error("Got the overpass response, but could not process it: ", e, e.stack) | ||||
|             return undefined | ||||
|         } finally { | ||||
|             self.retries.setData(0); | ||||
|             self.runningQuery.setData(false); | ||||
|             self.retries.setData(0) | ||||
|             self.runningQuery.setData(false) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -1,46 +1,42 @@ | |||
| import {Changes} from "../Osm/Changes"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import { Changes } from "../Osm/Changes" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| export default class PendingChangesUploader { | ||||
| 
 | ||||
|     private lastChange: Date; | ||||
|     private lastChange: Date | ||||
| 
 | ||||
|     constructor(changes: Changes, selectedFeature: UIEventSource<any>) { | ||||
|         const self = this; | ||||
|         this.lastChange = new Date(); | ||||
|         const self = this | ||||
|         this.lastChange = new Date() | ||||
|         changes.pendingChanges.addCallback(() => { | ||||
|             self.lastChange = new Date(); | ||||
|             self.lastChange = new Date() | ||||
| 
 | ||||
|             window.setTimeout(() => { | ||||
|                 const diff = (new Date().getTime() - self.lastChange.getTime()) / 1000; | ||||
|                 const diff = (new Date().getTime() - self.lastChange.getTime()) / 1000 | ||||
|                 if (Constants.updateTimeoutSec >= diff - 1) { | ||||
|                     changes.flushChanges("Flushing changes due to timeout"); | ||||
|                     changes.flushChanges("Flushing changes due to timeout") | ||||
|                 } | ||||
|             }, Constants.updateTimeoutSec * 1000); | ||||
|         }); | ||||
|             }, Constants.updateTimeoutSec * 1000) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         selectedFeature | ||||
|             .stabilized(10000) | ||||
|             .addCallback(feature => { | ||||
|         selectedFeature.stabilized(10000).addCallback((feature) => { | ||||
|             if (feature === undefined) { | ||||
|                 // The popup got closed - we flush
 | ||||
|                     changes.flushChanges("Flushing changes due to popup closed"); | ||||
|                 changes.flushChanges("Flushing changes due to popup closed") | ||||
|             } | ||||
|             }); | ||||
|         }) | ||||
| 
 | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         document.addEventListener('mouseout', e => { | ||||
|         document.addEventListener("mouseout", (e) => { | ||||
|             // @ts-ignore
 | ||||
|             if (!e.toElement && !e.relatedTarget) { | ||||
|                 changes.flushChanges("Flushing changes due to focus lost"); | ||||
|                 changes.flushChanges("Flushing changes due to focus lost") | ||||
|             } | ||||
|         }); | ||||
|         }) | ||||
| 
 | ||||
|         document.onfocus = () => { | ||||
|             changes.flushChanges("OnFocus") | ||||
|  | @ -50,28 +46,28 @@ export default class PendingChangesUploader { | |||
|             changes.flushChanges("OnFocus") | ||||
|         } | ||||
|         try { | ||||
|             document.addEventListener("visibilitychange", () => { | ||||
|             document.addEventListener( | ||||
|                 "visibilitychange", | ||||
|                 () => { | ||||
|                     changes.flushChanges("Visibility change") | ||||
|             }, false); | ||||
|                 }, | ||||
|                 false | ||||
|             ) | ||||
|         } catch (e) { | ||||
|             console.warn("Could not register visibility change listener", e) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         function onunload(e) { | ||||
|             if (changes.pendingChanges.data.length == 0) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             changes.flushChanges("onbeforeunload - probably closing or something similar"); | ||||
|             e.preventDefault(); | ||||
|             changes.flushChanges("onbeforeunload - probably closing or something similar") | ||||
|             e.preventDefault() | ||||
|             return "Saving your last changes..." | ||||
|         } | ||||
| 
 | ||||
|         window.onbeforeunload = onunload | ||||
|         // https://stackoverflow.com/questions/3239834/window-onbeforeunload-not-working-on-the-ipad#4824156
 | ||||
|         window.addEventListener("pagehide", onunload) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,51 +1,47 @@ | |||
| /** | ||||
|  * This actor will download the latest version of the selected element from OSM and update the tags if necessary. | ||||
|  */ | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {ElementStorage} from "../ElementStorage"; | ||||
| import {Changes} from "../Osm/Changes"; | ||||
| import {OsmObject} from "../Osm/OsmObject"; | ||||
| import {OsmConnection} from "../Osm/OsmConnection"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import SimpleMetaTagger from "../SimpleMetaTagger"; | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import { ElementStorage } from "../ElementStorage" | ||||
| import { Changes } from "../Osm/Changes" | ||||
| import { OsmObject } from "../Osm/OsmObject" | ||||
| import { OsmConnection } from "../Osm/OsmConnection" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import SimpleMetaTagger from "../SimpleMetaTagger" | ||||
| 
 | ||||
| export default class SelectedElementTagsUpdater { | ||||
| 
 | ||||
|     private static readonly metatags = new Set(["timestamp", | ||||
|     private static readonly metatags = new Set([ | ||||
|         "timestamp", | ||||
|         "version", | ||||
|         "changeset", | ||||
|         "user", | ||||
|         "uid", | ||||
|         "id"]) | ||||
|         "id", | ||||
|     ]) | ||||
| 
 | ||||
|     constructor(state: { | ||||
|         selectedElement: UIEventSource<any>, | ||||
|         allElements: ElementStorage, | ||||
|         changes: Changes, | ||||
|         osmConnection: OsmConnection, | ||||
|         selectedElement: UIEventSource<any> | ||||
|         allElements: ElementStorage | ||||
|         changes: Changes | ||||
|         osmConnection: OsmConnection | ||||
|         layoutToUse: LayoutConfig | ||||
|     }) { | ||||
| 
 | ||||
| 
 | ||||
|         state.osmConnection.isLoggedIn.addCallbackAndRun(isLoggedIn => { | ||||
|         state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { | ||||
|             if (isLoggedIn) { | ||||
|                 SelectedElementTagsUpdater.installCallback(state) | ||||
|                 return true; | ||||
|                 return true | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public static installCallback(state: { | ||||
|         selectedElement: UIEventSource<any>, | ||||
|         allElements: ElementStorage, | ||||
|         changes: Changes, | ||||
|         osmConnection: OsmConnection, | ||||
|         selectedElement: UIEventSource<any> | ||||
|         allElements: ElementStorage | ||||
|         changes: Changes | ||||
|         osmConnection: OsmConnection | ||||
|         layoutToUse: LayoutConfig | ||||
|     }) { | ||||
| 
 | ||||
| 
 | ||||
|         state.selectedElement.addCallbackAndRunD(s => { | ||||
|         state.selectedElement.addCallbackAndRunD((s) => { | ||||
|             let id = s.properties?.id | ||||
| 
 | ||||
|             const backendUrl = state.osmConnection._oauth_config.url | ||||
|  | @ -55,31 +51,31 @@ export default class SelectedElementTagsUpdater { | |||
| 
 | ||||
|             if (!(id.startsWith("way") || id.startsWith("node") || id.startsWith("relation"))) { | ||||
|                 // This object is _not_ from OSM, so we skip it!
 | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             if (id.indexOf("-") >= 0) { | ||||
|                 // This is a new object
 | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             OsmObject.DownloadPropertiesOf(id).then(latestTags => { | ||||
|             OsmObject.DownloadPropertiesOf(id).then((latestTags) => { | ||||
|                 SelectedElementTagsUpdater.applyUpdate(state, latestTags, id) | ||||
|             }) | ||||
| 
 | ||||
|         }); | ||||
| 
 | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public static applyUpdate(state: { | ||||
|                                   selectedElement: UIEventSource<any>, | ||||
|                                   allElements: ElementStorage, | ||||
|                                   changes: Changes, | ||||
|                                   osmConnection: OsmConnection, | ||||
|     public static applyUpdate( | ||||
|         state: { | ||||
|             selectedElement: UIEventSource<any> | ||||
|             allElements: ElementStorage | ||||
|             changes: Changes | ||||
|             osmConnection: OsmConnection | ||||
|             layoutToUse: LayoutConfig | ||||
|                               }, latestTags: any, id: string | ||||
|         }, | ||||
|         latestTags: any, | ||||
|         id: string | ||||
|     ) { | ||||
|         try { | ||||
| 
 | ||||
|             const leftRightSensitive = state.layoutToUse.isLeftRightSensitive() | ||||
| 
 | ||||
|             if (leftRightSensitive) { | ||||
|  | @ -87,11 +83,11 @@ export default class SelectedElementTagsUpdater { | |||
|             } | ||||
| 
 | ||||
|             const pendingChanges = state.changes.pendingChanges.data | ||||
|                 .filter(change => change.type + "/" + change.id === id) | ||||
|                 .filter(change => change.tags !== undefined); | ||||
|                 .filter((change) => change.type + "/" + change.id === id) | ||||
|                 .filter((change) => change.tags !== undefined) | ||||
| 
 | ||||
|             for (const pendingChange of pendingChanges) { | ||||
|                 const tagChanges = pendingChange.tags; | ||||
|                 const tagChanges = pendingChange.tags | ||||
|                 for (const tagChange of tagChanges) { | ||||
|                     const key = tagChange.k | ||||
|                     const v = tagChange.v | ||||
|  | @ -103,10 +99,9 @@ export default class SelectedElementTagsUpdater { | |||
|                 } | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             // With the changes applied, we merge them onto the upstream object
 | ||||
|             let somethingChanged = false; | ||||
|             const currentTagsSource = state.allElements.getEventSourceById(id); | ||||
|             let somethingChanged = false | ||||
|             const currentTagsSource = state.allElements.getEventSourceById(id) | ||||
|             const currentTags = currentTagsSource.data | ||||
|             for (const key in latestTags) { | ||||
|                 let osmValue = latestTags[key] | ||||
|  | @ -117,7 +112,7 @@ export default class SelectedElementTagsUpdater { | |||
| 
 | ||||
|                 const localValue = currentTags[key] | ||||
|                 if (localValue !== osmValue) { | ||||
|                     somethingChanged = true; | ||||
|                     somethingChanged = true | ||||
|                     currentTags[key] = osmValue | ||||
|                 } | ||||
|             } | ||||
|  | @ -137,7 +132,6 @@ export default class SelectedElementTagsUpdater { | |||
|                 somethingChanged = true | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             if (somethingChanged) { | ||||
|                 console.log("Detected upstream changes to the object when opening it, updating...") | ||||
|                 currentTagsSource.ping() | ||||
|  | @ -148,6 +142,4 @@ export default class SelectedElementTagsUpdater { | |||
|             console.error("Updating the tags of selected element ", id, "failed due to", e) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,63 +1,67 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {OsmObject} from "../Osm/OsmObject"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import {ElementStorage} from "../ElementStorage"; | ||||
| import FeaturePipeline from "../FeatureSource/FeaturePipeline"; | ||||
| import {GeoOperations} from "../GeoOperations"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import { OsmObject } from "../Osm/OsmObject" | ||||
| import Loc from "../../Models/Loc" | ||||
| import { ElementStorage } from "../ElementStorage" | ||||
| import FeaturePipeline from "../FeatureSource/FeaturePipeline" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| 
 | ||||
| /** | ||||
|  * Makes sure the hash shows the selected element and vice-versa. | ||||
|  */ | ||||
| export default class SelectedFeatureHandler { | ||||
|     private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "filters", "location_track", "", undefined]) | ||||
|     private readonly hash: UIEventSource<string>; | ||||
|     private static readonly _no_trigger_on = new Set([ | ||||
|         "welcome", | ||||
|         "copyright", | ||||
|         "layers", | ||||
|         "new", | ||||
|         "filters", | ||||
|         "location_track", | ||||
|         "", | ||||
|         undefined, | ||||
|     ]) | ||||
|     private readonly hash: UIEventSource<string> | ||||
|     private readonly state: { | ||||
|         selectedElement: UIEventSource<any>, | ||||
|         allElements: ElementStorage, | ||||
|         locationControl: UIEventSource<Loc>, | ||||
|         selectedElement: UIEventSource<any> | ||||
|         allElements: ElementStorage | ||||
|         locationControl: UIEventSource<Loc> | ||||
|         layoutToUse: LayoutConfig | ||||
|     } | ||||
| 
 | ||||
|     constructor( | ||||
|         hash: UIEventSource<string>, | ||||
|         state: { | ||||
|             selectedElement: UIEventSource<any>, | ||||
|             allElements: ElementStorage, | ||||
|             featurePipeline: FeaturePipeline, | ||||
|             locationControl: UIEventSource<Loc>, | ||||
|             selectedElement: UIEventSource<any> | ||||
|             allElements: ElementStorage | ||||
|             featurePipeline: FeaturePipeline | ||||
|             locationControl: UIEventSource<Loc> | ||||
|             layoutToUse: LayoutConfig | ||||
|         } | ||||
|     ) { | ||||
|         this.hash = hash; | ||||
|         this.hash = hash | ||||
|         this.state = state | ||||
| 
 | ||||
| 
 | ||||
|         // If the hash changes, set the selected element correctly
 | ||||
| 
 | ||||
|         const self = this; | ||||
|         const self = this | ||||
|         hash.addCallback(() => self.setSelectedElementFromHash()) | ||||
| 
 | ||||
| 
 | ||||
|         state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD(_ => { | ||||
|         state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD((_) => { | ||||
|             // New data was loaded. In initial startup, the hash might be set (via the URL) but might not be selected yet
 | ||||
|             if (hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)) { | ||||
|                 // This is an invalid hash anyway
 | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             if (state.selectedElement.data !== undefined) { | ||||
|                 // We already have something selected
 | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             self.setSelectedElementFromHash() | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         this.initialLoad() | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * On startup: check if the hash is loaded and eventually zoom to it | ||||
|      * @private | ||||
|  | @ -65,21 +69,18 @@ export default class SelectedFeatureHandler { | |||
|     private initialLoad() { | ||||
|         const hash = this.hash.data | ||||
|         if (hash === undefined || hash === "" || hash.indexOf("-") >= 0) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         if (SelectedFeatureHandler._no_trigger_on.has(hash)) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (!(hash.startsWith("node") || hash.startsWith("way") || hash.startsWith("relation"))) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         OsmObject.DownloadObjectAsync(hash).then(obj => { | ||||
| 
 | ||||
|         OsmObject.DownloadObjectAsync(hash).then((obj) => { | ||||
|             try { | ||||
| 
 | ||||
|                 console.log("Downloaded selected object from OSM-API for initial load: ", hash) | ||||
|                 const geojson = obj.asGeoJson() | ||||
|                 this.state.allElements.addOrGetElement(geojson) | ||||
|  | @ -88,9 +89,7 @@ export default class SelectedFeatureHandler { | |||
|             } catch (e) { | ||||
|                 console.error(e) | ||||
|             } | ||||
| 
 | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private setSelectedElementFromHash() { | ||||
|  | @ -98,22 +97,21 @@ export default class SelectedFeatureHandler { | |||
|         const h = this.hash.data | ||||
|         if (h === undefined || h === "") { | ||||
|             // Hash has been cleared - we clear the selected element
 | ||||
|             state.selectedElement.setData(undefined); | ||||
|             state.selectedElement.setData(undefined) | ||||
|         } else { | ||||
| 
 | ||||
|             // we search the element to select
 | ||||
|             const feature = state.allElements.ContainingFeatures.get(h) | ||||
|             if (feature === undefined) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             const currentlySeleced = state.selectedElement.data | ||||
|             if (currentlySeleced === undefined) { | ||||
|                 state.selectedElement.setData(feature) | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             if (currentlySeleced.properties?.id === feature.properties.id) { | ||||
|                 // We already have the right feature
 | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             state.selectedElement.setData(feature) | ||||
|         } | ||||
|  | @ -121,25 +119,24 @@ export default class SelectedFeatureHandler { | |||
| 
 | ||||
|     // If a feature is selected via the hash, zoom there
 | ||||
|     private zoomToSelectedFeature() { | ||||
| 
 | ||||
|         const selected = this.state.selectedElement.data | ||||
|         if (selected === undefined) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         const centerpoint = GeoOperations.centerpointCoordinates(selected) | ||||
|         const location = this.state.locationControl; | ||||
|         const location = this.state.locationControl | ||||
|         location.data.lon = centerpoint[0] | ||||
|         location.data.lat = centerpoint[1] | ||||
| 
 | ||||
|         const minZoom = Math.max(14, ...(this.state.layoutToUse?.layers?.map(l => l.minzoomVisible) ?? [])) | ||||
|         const minZoom = Math.max( | ||||
|             14, | ||||
|             ...(this.state.layoutToUse?.layers?.map((l) => l.minzoomVisible) ?? []) | ||||
|         ) | ||||
|         if (location.data.zoom < minZoom) { | ||||
|             location.data.zoom = minZoom | ||||
|         } | ||||
| 
 | ||||
|         location.ping(); | ||||
| 
 | ||||
| 
 | ||||
|         location.ping() | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,88 +1,87 @@ | |||
| import * as L from "leaflet"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import * as L from "leaflet" | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import Constants from "../../Models/Constants" | ||||
| import BaseUIElement from "../../UI/BaseUIElement" | ||||
| 
 | ||||
| /** | ||||
|  * The stray-click-hanlders adds a marker to the map if no feature was clicked. | ||||
|  * Shows the given uiToShow-element in the messagebox | ||||
|  */ | ||||
| export default class StrayClickHandler { | ||||
|     private _lastMarker; | ||||
|     private _lastMarker | ||||
| 
 | ||||
|     constructor( | ||||
|         state: { | ||||
|             LastClickLocation: UIEventSource<{ lat: number, lon: number }>, | ||||
|             selectedElement: UIEventSource<string>, | ||||
|             filteredLayers: UIEventSource<FilteredLayer[]>, | ||||
|             LastClickLocation: UIEventSource<{ lat: number; lon: number }> | ||||
|             selectedElement: UIEventSource<string> | ||||
|             filteredLayers: UIEventSource<FilteredLayer[]> | ||||
|             leafletMap: UIEventSource<L.Map> | ||||
|         }, | ||||
|         uiToShow: ScrollableFullScreen, | ||||
|         iconToShow: BaseUIElement) { | ||||
|         const self = this; | ||||
|         iconToShow: BaseUIElement | ||||
|     ) { | ||||
|         const self = this | ||||
|         const leafletMap = state.leafletMap | ||||
|         state.filteredLayers.data.forEach((filteredLayer) => { | ||||
|             filteredLayer.isDisplayed.addCallback(isEnabled => { | ||||
|             filteredLayer.isDisplayed.addCallback((isEnabled) => { | ||||
|                 if (isEnabled && self._lastMarker && leafletMap.data !== undefined) { | ||||
|                     // When a layer is activated, we remove the 'last click location' in order to force the user to reclick
 | ||||
|                     // This reclick might be at a location where a feature now appeared...
 | ||||
|                     state.leafletMap.data.removeLayer(self._lastMarker); | ||||
|                     state.leafletMap.data.removeLayer(self._lastMarker) | ||||
|                 } | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         state.LastClickLocation.addCallback(function (lastClick) { | ||||
| 
 | ||||
|             if (self._lastMarker !== undefined) { | ||||
|                 state.leafletMap.data?.removeLayer(self._lastMarker); | ||||
|                 state.leafletMap.data?.removeLayer(self._lastMarker) | ||||
|             } | ||||
| 
 | ||||
|             if (lastClick === undefined) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             state.selectedElement.setData(undefined); | ||||
|             state.selectedElement.setData(undefined) | ||||
|             const clickCoor: [number, number] = [lastClick.lat, lastClick.lon] | ||||
|             self._lastMarker = L.marker(clickCoor, { | ||||
|                 icon: L.divIcon({ | ||||
|                     html: iconToShow.ConstructElement(), | ||||
|                     iconSize: [50, 50], | ||||
|                     iconAnchor: [25, 50], | ||||
|                     popupAnchor: [0, -45] | ||||
|                     popupAnchor: [0, -45], | ||||
|                 }), | ||||
|             }) | ||||
|             }); | ||||
|             const popup = L.popup({ | ||||
|                 autoPan: true, | ||||
|                 autoPanPaddingTopLeft: [15, 15], | ||||
|                 closeOnEscapeKey: true, | ||||
|                 autoClose: true | ||||
|             }).setContent("<div id='strayclick' style='height: 65vh'></div>"); | ||||
|             self._lastMarker.addTo(leafletMap.data); | ||||
|             self._lastMarker.bindPopup(popup); | ||||
|                 autoClose: true, | ||||
|             }).setContent("<div id='strayclick' style='height: 65vh'></div>") | ||||
|             self._lastMarker.addTo(leafletMap.data) | ||||
|             self._lastMarker.bindPopup(popup) | ||||
| 
 | ||||
|             self._lastMarker.on("click", () => { | ||||
|                 if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) { | ||||
|                     self._lastMarker.closePopup() | ||||
|                     leafletMap.data.flyTo(clickCoor, Constants.userJourney.minZoomLevelToAddNewPoints) | ||||
|                     return; | ||||
|                     leafletMap.data.flyTo( | ||||
|                         clickCoor, | ||||
|                         Constants.userJourney.minZoomLevelToAddNewPoints | ||||
|                     ) | ||||
|                     return | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 uiToShow.AttachTo("strayclick") | ||||
|                 uiToShow.Activate(); | ||||
|             }); | ||||
|         }); | ||||
|                 uiToShow.Activate() | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         state.selectedElement.addCallback(() => { | ||||
|             if (self._lastMarker !== undefined) { | ||||
|                 leafletMap.data.removeLayer(self._lastMarker); | ||||
|                 this._lastMarker = undefined; | ||||
|                 leafletMap.data.removeLayer(self._lastMarker) | ||||
|                 this._lastMarker = undefined | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,19 +1,19 @@ | |||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import Locale from "../../UI/i18n/Locale"; | ||||
| import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"; | ||||
| import Combine from "../../UI/Base/Combine"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {ElementStorage} from "../ElementStorage"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import Locale from "../../UI/i18n/Locale" | ||||
| import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer" | ||||
| import Combine from "../../UI/Base/Combine" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import { ElementStorage } from "../ElementStorage" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| export default class TitleHandler { | ||||
|     constructor(state: { | ||||
|         selectedElement: Store<any>, | ||||
|         layoutToUse: LayoutConfig, | ||||
|         selectedElement: Store<any> | ||||
|         layoutToUse: LayoutConfig | ||||
|         allElements: ElementStorage | ||||
|     }) { | ||||
|         const currentTitle: Store<string> = state.selectedElement.map( | ||||
|             selected => { | ||||
|             (selected) => { | ||||
|                 const layout = state.layoutToUse | ||||
|                 const defaultTitle = layout?.title?.txt ?? "MapComplete" | ||||
| 
 | ||||
|  | @ -21,23 +21,28 @@ export default class TitleHandler { | |||
|                     return defaultTitle | ||||
|                 } | ||||
| 
 | ||||
|                 const tags = selected.properties; | ||||
|                 const tags = selected.properties | ||||
|                 for (const layer of layout.layers) { | ||||
|                     if (layer.title === undefined) { | ||||
|                         continue; | ||||
|                         continue | ||||
|                     } | ||||
|                     if (layer.source.osmTags.matchesProperties(tags)) { | ||||
|                         const tagsSource = state.allElements.getEventSourceById(tags.id) ?? new UIEventSource<any>(tags) | ||||
|                         const tagsSource = | ||||
|                             state.allElements.getEventSourceById(tags.id) ?? | ||||
|                             new UIEventSource<any>(tags) | ||||
|                         const title = new TagRenderingAnswer(tagsSource, layer.title, {}) | ||||
|                         return new Combine([defaultTitle, " | ", title]).ConstructElement()?.textContent ?? defaultTitle; | ||||
|                         return ( | ||||
|                             new Combine([defaultTitle, " | ", title]).ConstructElement() | ||||
|                                 ?.textContent ?? defaultTitle | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|                 return defaultTitle | ||||
|             }, [Locale.language] | ||||
|             }, | ||||
|             [Locale.language] | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|         currentTitle.addCallbackAndRunD(title => { | ||||
|         currentTitle.addCallbackAndRunD((title) => { | ||||
|             if (Utils.runningFromConsole) { | ||||
|                 return | ||||
|             } | ||||
|  |  | |||
							
								
								
									
										148
									
								
								Logic/BBox.ts
									
										
									
									
									
								
							
							
						
						
									
										148
									
								
								Logic/BBox.ts
									
										
									
									
									
								
							|  | @ -1,31 +1,32 @@ | |||
| import * as turf from "@turf/turf"; | ||||
| import {TileRange, Tiles} from "../Models/TileRange"; | ||||
| import {GeoOperations} from "./GeoOperations"; | ||||
| import * as turf from "@turf/turf" | ||||
| import { TileRange, Tiles } from "../Models/TileRange" | ||||
| import { GeoOperations } from "./GeoOperations" | ||||
| 
 | ||||
| export class BBox { | ||||
| 
 | ||||
|     static global: BBox = new BBox([[-180, -90], [180, 90]]); | ||||
|     readonly maxLat: number; | ||||
|     readonly maxLon: number; | ||||
|     readonly minLat: number; | ||||
|     readonly minLon: number; | ||||
|     static global: BBox = new BBox([ | ||||
|         [-180, -90], | ||||
|         [180, 90], | ||||
|     ]) | ||||
|     readonly maxLat: number | ||||
|     readonly maxLon: number | ||||
|     readonly minLat: number | ||||
|     readonly minLon: number | ||||
| 
 | ||||
|     /*** | ||||
|      * Coordinates should be [[lon, lat],[lon, lat]] | ||||
|      * @param coordinates | ||||
|      */ | ||||
|     constructor(coordinates) { | ||||
|         this.maxLat = -90; | ||||
|         this.maxLon = -180; | ||||
|         this.minLat = 90; | ||||
|         this.minLon = 180; | ||||
| 
 | ||||
|         this.maxLat = -90 | ||||
|         this.maxLon = -180 | ||||
|         this.minLat = 90 | ||||
|         this.minLon = 180 | ||||
| 
 | ||||
|         for (const coordinate of coordinates) { | ||||
|             this.maxLon = Math.max(this.maxLon, coordinate[0]); | ||||
|             this.maxLat = Math.max(this.maxLat, coordinate[1]); | ||||
|             this.minLon = Math.min(this.minLon, coordinate[0]); | ||||
|             this.minLat = Math.min(this.minLat, coordinate[1]); | ||||
|             this.maxLon = Math.max(this.maxLon, coordinate[0]) | ||||
|             this.maxLat = Math.max(this.maxLat, coordinate[1]) | ||||
|             this.minLon = Math.min(this.minLon, coordinate[0]) | ||||
|             this.minLat = Math.min(this.minLat, coordinate[1]) | ||||
|         } | ||||
| 
 | ||||
|         this.maxLon = Math.min(this.maxLon, 180) | ||||
|  | @ -33,27 +34,32 @@ export class BBox { | |||
|         this.minLon = Math.max(this.minLon, -180) | ||||
|         this.minLat = Math.max(this.minLat, -90) | ||||
| 
 | ||||
| 
 | ||||
|         this.check(); | ||||
|         this.check() | ||||
|     } | ||||
| 
 | ||||
|     static fromLeafletBounds(bounds) { | ||||
|         return new BBox([[bounds.getWest(), bounds.getNorth()], [bounds.getEast(), bounds.getSouth()]]) | ||||
|         return new BBox([ | ||||
|             [bounds.getWest(), bounds.getNorth()], | ||||
|             [bounds.getEast(), bounds.getSouth()], | ||||
|         ]) | ||||
|     } | ||||
| 
 | ||||
|     static get(feature): BBox { | ||||
|         if (feature.bbox?.overlapsWith === undefined) { | ||||
|             const turfBbox: number[] = turf.bbox(feature) | ||||
|             feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]); | ||||
|             feature.bbox = new BBox([ | ||||
|                 [turfBbox[0], turfBbox[1]], | ||||
|                 [turfBbox[2], turfBbox[3]], | ||||
|             ]) | ||||
|         } | ||||
|         return feature.bbox; | ||||
|         return feature.bbox | ||||
|     } | ||||
| 
 | ||||
|     static bboxAroundAll(bboxes: BBox[]): BBox { | ||||
|         let maxLat: number = -90; | ||||
|         let maxLon: number = -180; | ||||
|         let minLat: number = 80; | ||||
|         let minLon: number = 180; | ||||
|         let maxLat: number = -90 | ||||
|         let maxLon: number = -180 | ||||
|         let minLat: number = 80 | ||||
|         let minLon: number = 180 | ||||
| 
 | ||||
|         for (const bbox of bboxes) { | ||||
|             maxLat = Math.max(maxLat, bbox.maxLat) | ||||
|  | @ -61,7 +67,10 @@ export class BBox { | |||
|             minLat = Math.min(minLat, bbox.minLat) | ||||
|             minLon = Math.min(minLon, bbox.minLon) | ||||
|         } | ||||
|         return new BBox([[maxLon, maxLat], [minLon, minLat]]) | ||||
|         return new BBox([ | ||||
|             [maxLon, maxLat], | ||||
|             [minLon, minLat], | ||||
|         ]) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -85,11 +94,10 @@ export class BBox { | |||
|     } | ||||
| 
 | ||||
|     public unionWith(other: BBox) { | ||||
|         return new BBox([[ | ||||
|             Math.max(this.maxLon, other.maxLon), | ||||
|             Math.max(this.maxLat, other.maxLat)], | ||||
|             [Math.min(this.minLon, other.minLon), | ||||
|                 Math.min(this.minLat, other.minLat)]]) | ||||
|         return new BBox([ | ||||
|             [Math.max(this.maxLon, other.maxLon), Math.max(this.maxLat, other.maxLat)], | ||||
|             [Math.min(this.minLon, other.minLon), Math.min(this.minLat, other.minLat)], | ||||
|         ]) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -102,32 +110,31 @@ export class BBox { | |||
| 
 | ||||
|     public overlapsWith(other: BBox) { | ||||
|         if (this.maxLon < other.minLon) { | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
|         if (this.maxLat < other.minLat) { | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
|         if (this.minLon > other.maxLon) { | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
|         return this.minLat <= other.maxLat; | ||||
| 
 | ||||
|         return this.minLat <= other.maxLat | ||||
|     } | ||||
| 
 | ||||
|     public isContainedIn(other: BBox) { | ||||
|         if (this.maxLon > other.maxLon) { | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
|         if (this.maxLat > other.maxLat) { | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
|         if (this.minLon < other.minLon) { | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
|         if (this.minLat < other.minLat) { | ||||
|             return false | ||||
|         } | ||||
|         return true; | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     getEast() { | ||||
|  | @ -147,32 +154,35 @@ export class BBox { | |||
|     } | ||||
| 
 | ||||
|     contains(lonLat: [number, number]) { | ||||
|         return this.minLat <= lonLat[1] && lonLat[1] <= this.maxLat | ||||
|             && this.minLon <= lonLat[0] && lonLat[0] <= this.maxLon | ||||
|         return ( | ||||
|             this.minLat <= lonLat[1] && | ||||
|             lonLat[1] <= this.maxLat && | ||||
|             this.minLon <= lonLat[0] && | ||||
|             lonLat[0] <= this.maxLon | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     pad(factor: number, maxIncrease = 2): BBox { | ||||
| 
 | ||||
|         const latDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLat - this.minLat) * factor) | ||||
|         const lonDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor) | ||||
|         return new BBox([[ | ||||
|             this.minLon - lonDiff, | ||||
|             this.minLat - latDiff | ||||
|         ], [this.maxLon + lonDiff, | ||||
|             this.maxLat + latDiff]]) | ||||
|         return new BBox([ | ||||
|             [this.minLon - lonDiff, this.minLat - latDiff], | ||||
|             [this.maxLon + lonDiff, this.maxLat + latDiff], | ||||
|         ]) | ||||
|     } | ||||
| 
 | ||||
|     padAbsolute(degrees: number): BBox { | ||||
| 
 | ||||
|         return new BBox([[ | ||||
|             this.minLon - degrees, | ||||
|             this.minLat - degrees | ||||
|         ], [this.maxLon + degrees, | ||||
|             this.maxLat + degrees]]) | ||||
|         return new BBox([ | ||||
|             [this.minLon - degrees, this.minLat - degrees], | ||||
|             [this.maxLon + degrees, this.maxLat + degrees], | ||||
|         ]) | ||||
|     } | ||||
| 
 | ||||
|     toLeaflet() { | ||||
|         return [[this.minLat, this.minLon], [this.maxLat, this.maxLon]] | ||||
|         return [ | ||||
|             [this.minLat, this.minLon], | ||||
|             [this.maxLat, this.maxLon], | ||||
|         ] | ||||
|     } | ||||
| 
 | ||||
|     asGeoJson(properties: any): any { | ||||
|  | @ -181,16 +191,16 @@ export class BBox { | |||
|             properties: properties, | ||||
|             geometry: { | ||||
|                 type: "Polygon", | ||||
|                 coordinates: [[ | ||||
| 
 | ||||
|                 coordinates: [ | ||||
|                     [ | ||||
|                         [this.minLon, this.minLat], | ||||
|                         [this.maxLon, this.minLat], | ||||
|                         [this.maxLon, this.maxLat], | ||||
|                         [this.minLon, this.maxLat], | ||||
|                         [this.minLon, this.minLat], | ||||
| 
 | ||||
|                 ]] | ||||
|             } | ||||
|                     ], | ||||
|                 ], | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -206,22 +216,22 @@ export class BBox { | |||
|         return new BBox([].concat(boundsul, boundslr)) | ||||
|     } | ||||
| 
 | ||||
|     toMercator(): { minLat: number, maxLat: number, minLon: number, maxLon: number } { | ||||
|     toMercator(): { minLat: number; maxLat: number; minLon: number; maxLon: number } { | ||||
|         const [minLon, minLat] = GeoOperations.ConvertWgs84To900913([this.minLon, this.minLat]) | ||||
|         const [maxLon, maxLat] = GeoOperations.ConvertWgs84To900913([this.maxLon, this.maxLat]) | ||||
| 
 | ||||
|         return { | ||||
|             minLon, maxLon, | ||||
|             minLat, maxLat | ||||
|             minLon, | ||||
|             maxLon, | ||||
|             minLat, | ||||
|             maxLat, | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private check() { | ||||
|         if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) { | ||||
|             console.trace("BBox with NaN detected:", this); | ||||
|             throw  "BBOX has NAN"; | ||||
|             console.trace("BBox with NaN detected:", this) | ||||
|             throw "BBOX has NAN" | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,46 +1,56 @@ | |||
| /// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
 | ||||
| import {Store, UIEventSource} from "./UIEventSource"; | ||||
| import FeaturePipeline from "./FeatureSource/FeaturePipeline"; | ||||
| import Loc from "../Models/Loc"; | ||||
| import {BBox} from "./BBox"; | ||||
| import { Store, UIEventSource } from "./UIEventSource" | ||||
| import FeaturePipeline from "./FeatureSource/FeaturePipeline" | ||||
| import Loc from "../Models/Loc" | ||||
| import { BBox } from "./BBox" | ||||
| 
 | ||||
| export default class ContributorCount { | ||||
|     public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource< | ||||
|         Map<string, number> | ||||
|     >(new Map<string, number>()) | ||||
|     private readonly state: { | ||||
|         featurePipeline: FeaturePipeline | ||||
|         currentBounds: Store<BBox> | ||||
|         locationControl: Store<Loc> | ||||
|     } | ||||
|     private lastUpdate: Date = undefined | ||||
| 
 | ||||
|     public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>()); | ||||
|     private readonly state: { featurePipeline: FeaturePipeline, currentBounds: Store<BBox>, locationControl: Store<Loc> }; | ||||
|     private lastUpdate: Date = undefined; | ||||
| 
 | ||||
|     constructor(state: { featurePipeline: FeaturePipeline, currentBounds: Store<BBox>, locationControl: Store<Loc> }) { | ||||
|         this.state = state; | ||||
|         const self = this; | ||||
|         state.currentBounds.map(bbox => { | ||||
|     constructor(state: { | ||||
|         featurePipeline: FeaturePipeline | ||||
|         currentBounds: Store<BBox> | ||||
|         locationControl: Store<Loc> | ||||
|     }) { | ||||
|         this.state = state | ||||
|         const self = this | ||||
|         state.currentBounds.map((bbox) => { | ||||
|             self.update(bbox) | ||||
|         }) | ||||
|         state.featurePipeline.runningQuery.addCallbackAndRun( | ||||
|             _ => self.update(state.currentBounds.data) | ||||
|         state.featurePipeline.runningQuery.addCallbackAndRun((_) => | ||||
|             self.update(state.currentBounds.data) | ||||
|         ) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private update(bbox: BBox) { | ||||
|         if (bbox === undefined) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         const now = new Date(); | ||||
|         if (this.lastUpdate !== undefined && ((now.getTime() - this.lastUpdate.getTime()) < 1000 * 60)) { | ||||
|             return; | ||||
|         const now = new Date() | ||||
|         if ( | ||||
|             this.lastUpdate !== undefined && | ||||
|             now.getTime() - this.lastUpdate.getTime() < 1000 * 60 | ||||
|         ) { | ||||
|             return | ||||
|         } | ||||
|         this.lastUpdate = now; | ||||
|         this.lastUpdate = now | ||||
|         const featuresList = this.state.featurePipeline.GetAllFeaturesWithin(bbox) | ||||
|         const hist = new Map<string, number>(); | ||||
|         const hist = new Map<string, number>() | ||||
|         for (const list of featuresList) { | ||||
|             for (const feature of list) { | ||||
|                 const contributor = feature.properties["_last_edit:contributor"] | ||||
|                 const count = hist.get(contributor) ?? 0; | ||||
|                 const count = hist.get(contributor) ?? 0 | ||||
|                 hist.set(contributor, count + 1) | ||||
|             } | ||||
|         } | ||||
|         this.Contributors.setData(hist) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,35 +1,37 @@ | |||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||
| import {QueryParameters} from "./Web/QueryParameters"; | ||||
| import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; | ||||
| import {FixedUiElement} from "../UI/Base/FixedUiElement"; | ||||
| import {Utils} from "../Utils"; | ||||
| import Combine from "../UI/Base/Combine"; | ||||
| import {SubtleButton} from "../UI/Base/SubtleButton"; | ||||
| import BaseUIElement from "../UI/BaseUIElement"; | ||||
| import {UIEventSource} from "./UIEventSource"; | ||||
| import {LocalStorageSource} from "./Web/LocalStorageSource"; | ||||
| import LZString from "lz-string"; | ||||
| import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; | ||||
| import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | ||||
| import SharedTagRenderings from "../Customizations/SharedTagRenderings"; | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | ||||
| import { QueryParameters } from "./Web/QueryParameters" | ||||
| import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" | ||||
| import { FixedUiElement } from "../UI/Base/FixedUiElement" | ||||
| import { Utils } from "../Utils" | ||||
| import Combine from "../UI/Base/Combine" | ||||
| import { SubtleButton } from "../UI/Base/SubtleButton" | ||||
| import BaseUIElement from "../UI/BaseUIElement" | ||||
| import { UIEventSource } from "./UIEventSource" | ||||
| import { LocalStorageSource } from "./Web/LocalStorageSource" | ||||
| import LZString from "lz-string" | ||||
| import { FixLegacyTheme } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert" | ||||
| import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" | ||||
| import SharedTagRenderings from "../Customizations/SharedTagRenderings" | ||||
| import * as known_layers from "../assets/generated/known_layers.json" | ||||
| import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme"; | ||||
| import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme" | ||||
| import * as licenses from "../assets/generated/license_info.json" | ||||
| import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; | ||||
| import {FixImages} from "../Models/ThemeConfig/Conversion/FixImages"; | ||||
| import Svg from "../Svg"; | ||||
| import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" | ||||
| import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages" | ||||
| import Svg from "../Svg" | ||||
| 
 | ||||
| export default class DetermineLayout { | ||||
| 
 | ||||
|     private static readonly _knownImages =new Set( Array.from(licenses).map(l => l.path)) | ||||
|     private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path)) | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the correct layout for this website | ||||
|      */ | ||||
|     public static async GetLayout(): Promise<LayoutConfig> { | ||||
| 
 | ||||
|         const loadCustomThemeParam = QueryParameters.GetQueryParameter("userlayout", "false", "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme") | ||||
|         const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data); | ||||
|         const loadCustomThemeParam = QueryParameters.GetQueryParameter( | ||||
|             "userlayout", | ||||
|             "false", | ||||
|             "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme" | ||||
|         ) | ||||
|         const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data) | ||||
| 
 | ||||
|         if (layoutFromBase64.startsWith("http")) { | ||||
|             return await DetermineLayout.LoadRemoteTheme(layoutFromBase64) | ||||
|  | @ -42,100 +44,111 @@ export default class DetermineLayout { | |||
| 
 | ||||
|         let layoutId: string = undefined | ||||
| 
 | ||||
|         const path = window.location.pathname.split("/").slice(-1)[0]; | ||||
|         const path = window.location.pathname.split("/").slice(-1)[0] | ||||
|         if (path !== "theme.html" && path !== "") { | ||||
|             layoutId = path; | ||||
|             layoutId = path | ||||
|             if (path.endsWith(".html")) { | ||||
|                 layoutId = path.substr(0, path.length - 5); | ||||
|                 layoutId = path.substr(0, path.length - 5) | ||||
|             } | ||||
|             console.log("Using layout", layoutId); | ||||
|             console.log("Using layout", layoutId) | ||||
|         } | ||||
|         layoutId = QueryParameters.GetQueryParameter("layout", layoutId, "The layout to load into MapComplete").data; | ||||
|         layoutId = QueryParameters.GetQueryParameter( | ||||
|             "layout", | ||||
|             layoutId, | ||||
|             "The layout to load into MapComplete" | ||||
|         ).data | ||||
|         return AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase()) | ||||
|     } | ||||
| 
 | ||||
|     public static LoadLayoutFromHash( | ||||
|         userLayoutParam: UIEventSource<string> | ||||
|     ): LayoutConfig | null { | ||||
|         let hash = location.hash.substr(1); | ||||
|         let json: any; | ||||
|     public static LoadLayoutFromHash(userLayoutParam: UIEventSource<string>): LayoutConfig | null { | ||||
|         let hash = location.hash.substr(1) | ||||
|         let json: any | ||||
| 
 | ||||
|         try { | ||||
|             // layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
 | ||||
|             const dedicatedHashFromLocalStorage = LocalStorageSource.Get( | ||||
|                 "user-layout-" + userLayoutParam.data?.replace(" ", "_") | ||||
|             ); | ||||
|             ) | ||||
|             if (dedicatedHashFromLocalStorage.data?.length < 10) { | ||||
|                 dedicatedHashFromLocalStorage.setData(undefined); | ||||
|                 dedicatedHashFromLocalStorage.setData(undefined) | ||||
|             } | ||||
| 
 | ||||
|             const hashFromLocalStorage = LocalStorageSource.Get( | ||||
|                 "last-loaded-user-layout" | ||||
|             ); | ||||
|             const hashFromLocalStorage = LocalStorageSource.Get("last-loaded-user-layout") | ||||
|             if (hash.length < 10) { | ||||
|                 hash = | ||||
|                     dedicatedHashFromLocalStorage.data ?? | ||||
|                     hashFromLocalStorage.data; | ||||
|                 hash = dedicatedHashFromLocalStorage.data ?? hashFromLocalStorage.data | ||||
|             } else { | ||||
|                 console.log("Saving hash to local storage"); | ||||
|                 hashFromLocalStorage.setData(hash); | ||||
|                 dedicatedHashFromLocalStorage.setData(hash); | ||||
|                 console.log("Saving hash to local storage") | ||||
|                 hashFromLocalStorage.setData(hash) | ||||
|                 dedicatedHashFromLocalStorage.setData(hash) | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 json = JSON.parse(atob(hash)); | ||||
|                 json = JSON.parse(atob(hash)) | ||||
|             } catch (e) { | ||||
|                 // We try to decode with lz-string
 | ||||
|                 try { | ||||
|                     json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash))) | ||||
|                 } catch (e) { | ||||
|                     console.error(e) | ||||
|                     DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON")) | ||||
|                     return null; | ||||
|                     DetermineLayout.ShowErrorOnCustomTheme( | ||||
|                         "Could not decode the hash", | ||||
|                         new FixedUiElement("Not a valid (LZ-compressed) JSON") | ||||
|                     ) | ||||
|                     return null | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             const layoutToUse = DetermineLayout.prepCustomTheme(json) | ||||
|             userLayoutParam.setData(layoutToUse.id); | ||||
|             userLayoutParam.setData(layoutToUse.id) | ||||
|             return layoutToUse | ||||
|         } catch (e) { | ||||
|             console.error(e) | ||||
|             if (hash === undefined || hash.length < 10) { | ||||
|                 DetermineLayout.ShowErrorOnCustomTheme("Could not load a theme from the hash", new FixedUiElement("Hash does not contain data"), json) | ||||
|                 DetermineLayout.ShowErrorOnCustomTheme( | ||||
|                     "Could not load a theme from the hash", | ||||
|                     new FixedUiElement("Hash does not contain data"), | ||||
|                     json | ||||
|                 ) | ||||
|             } | ||||
|             this.ShowErrorOnCustomTheme("Could not parse the hash", new FixedUiElement(e), json) | ||||
|             return null; | ||||
|             return null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static ShowErrorOnCustomTheme( | ||||
|         intro: string = "Error: could not parse the custom layout:", | ||||
|         error: BaseUIElement, | ||||
|         json?: any) { | ||||
|         json?: any | ||||
|     ) { | ||||
|         new Combine([ | ||||
|             intro, | ||||
|             error.SetClass("alert"), | ||||
|             new SubtleButton(Svg.back_svg(), | ||||
|                 "Go back to the theme overview", | ||||
|                 {url: window.location.protocol + "//" + window.location.host + "/index.html", newTab: false}), | ||||
|             json !== undefined ? new SubtleButton(Svg.download_svg(),"Download the JSON file").onClick(() => { | ||||
|                 Utils.offerContentsAsDownloadableFile(JSON.stringify(json, null, "  "), "theme_definition.json") | ||||
|             }) : undefined | ||||
|             new SubtleButton(Svg.back_svg(), "Go back to the theme overview", { | ||||
|                 url: window.location.protocol + "//" + window.location.host + "/index.html", | ||||
|                 newTab: false, | ||||
|             }), | ||||
|             json !== undefined | ||||
|                 ? new SubtleButton(Svg.download_svg(), "Download the JSON file").onClick(() => { | ||||
|                       Utils.offerContentsAsDownloadableFile( | ||||
|                           JSON.stringify(json, null, "  "), | ||||
|                           "theme_definition.json" | ||||
|                       ) | ||||
|                   }) | ||||
|                 : undefined, | ||||
|         ]) | ||||
|             .SetClass("flex flex-col clickable") | ||||
|             .AttachTo("centermessage"); | ||||
|             .AttachTo("centermessage") | ||||
|     } | ||||
| 
 | ||||
|     private static prepCustomTheme(json: any, sourceUrl?: string, forceId?: string): LayoutConfig { | ||||
|          | ||||
|         if(json.layers === undefined && json.tagRenderings !== undefined){ | ||||
|             const iconTr = json.mapRendering.map(mr => mr.icon).find(icon => icon !== undefined) | ||||
|         if (json.layers === undefined && json.tagRenderings !== undefined) { | ||||
|             const iconTr = json.mapRendering.map((mr) => mr.icon).find((icon) => icon !== undefined) | ||||
|             const icon = new TagRenderingConfig(iconTr).render.txt | ||||
|             json = { | ||||
|                 id: json.id, | ||||
|                 description: json.description, | ||||
|                 descriptionTail: { | ||||
|                     en: "<div class='alert'>Layer only mode.</div> The loaded custom theme actually isn't a custom theme, but only contains a layer." | ||||
|                     en: "<div class='alert'>Layer only mode.</div> The loaded custom theme actually isn't a custom theme, but only contains a layer.", | ||||
|                 }, | ||||
|                 icon, | ||||
|                 title: json.name, | ||||
|  | @ -146,18 +159,21 @@ export default class DetermineLayout { | |||
|         const knownLayersDict = new Map<string, LayerConfigJson>() | ||||
|         for (const key in known_layers.layers) { | ||||
|             const layer = known_layers.layers[key] | ||||
|             knownLayersDict.set(layer.id,<LayerConfigJson> layer) | ||||
|             knownLayersDict.set(layer.id, <LayerConfigJson>layer) | ||||
|         } | ||||
|         const converState = { | ||||
|             tagRenderings: SharedTagRenderings.SharedTagRenderingJson, | ||||
|             sharedLayers: knownLayersDict, | ||||
|             publicLayers: new Set<string>() | ||||
|             publicLayers: new Set<string>(), | ||||
|         } | ||||
|         json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme") | ||||
|         const raw = json; | ||||
|         const raw = json | ||||
| 
 | ||||
|         json = new FixImages(DetermineLayout._knownImages).convertStrict(json, "While fixing the images") | ||||
|         json.enableNoteImports = json.enableNoteImports ?? false; | ||||
|         json = new FixImages(DetermineLayout._knownImages).convertStrict( | ||||
|             json, | ||||
|             "While fixing the images" | ||||
|         ) | ||||
|         json.enableNoteImports = json.enableNoteImports ?? false | ||||
|         json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme") | ||||
|         console.log("The layoutconfig is ", json) | ||||
| 
 | ||||
|  | @ -165,27 +181,27 @@ export default class DetermineLayout { | |||
| 
 | ||||
|         return new LayoutConfig(json, false, { | ||||
|             definitionRaw: JSON.stringify(raw, null, "  "), | ||||
|             definedAtUrl: sourceUrl | ||||
|             definedAtUrl: sourceUrl, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> { | ||||
|         console.log("Downloading map theme from ", link); | ||||
|         console.log("Downloading map theme from ", link) | ||||
| 
 | ||||
|         new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`) | ||||
|             .AttachTo("centermessage"); | ||||
|         new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`).AttachTo( | ||||
|             "centermessage" | ||||
|         ) | ||||
| 
 | ||||
|         try { | ||||
| 
 | ||||
|             let parsed = await Utils.downloadJson(link) | ||||
|             try { | ||||
|                 let forcedId = parsed.id | ||||
|                 const url = new URL(link) | ||||
|                 if(!(url.hostname === "localhost" || url.hostname === "127.0.0.1")){ | ||||
|                     forcedId = link; | ||||
|                 if (!(url.hostname === "localhost" || url.hostname === "127.0.0.1")) { | ||||
|                     forcedId = link | ||||
|                 } | ||||
|                 console.log("Loaded remote link:", link) | ||||
|                 return DetermineLayout.prepCustomTheme(parsed, link, forcedId); | ||||
|                 return DetermineLayout.prepCustomTheme(parsed, link, forcedId) | ||||
|             } catch (e) { | ||||
|                 console.error(e) | ||||
|                 DetermineLayout.ShowErrorOnCustomTheme( | ||||
|  | @ -193,17 +209,15 @@ export default class DetermineLayout { | |||
|                     new FixedUiElement(e), | ||||
|                     parsed | ||||
|                 ) | ||||
|                 return null; | ||||
|                 return null | ||||
|             } | ||||
| 
 | ||||
|         } catch (e) { | ||||
|             console.error(e) | ||||
|             DetermineLayout.ShowErrorOnCustomTheme( | ||||
|                 `<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`, | ||||
|                 new FixedUiElement(e) | ||||
|             ) | ||||
|             return null; | ||||
|             return null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,20 +1,17 @@ | |||
| /** | ||||
|  * Keeps track of a dictionary 'elementID' -> UIEventSource<tags> | ||||
|  */ | ||||
| import {UIEventSource} from "./UIEventSource"; | ||||
| import {GeoJSONObject} from "@turf/turf"; | ||||
| import { UIEventSource } from "./UIEventSource" | ||||
| import { GeoJSONObject } from "@turf/turf" | ||||
| 
 | ||||
| export class ElementStorage { | ||||
|     public ContainingFeatures = new Map<string, any>() | ||||
|     private _elements = new Map<string, UIEventSource<any>>() | ||||
| 
 | ||||
|     public ContainingFeatures = new Map<string, any>(); | ||||
|     private _elements = new Map<string, UIEventSource<any>>(); | ||||
| 
 | ||||
|     constructor() { | ||||
| 
 | ||||
|     } | ||||
|     constructor() {} | ||||
| 
 | ||||
|     addElementById(id: string, eventSource: UIEventSource<any>) { | ||||
|         this._elements.set(id, eventSource); | ||||
|         this._elements.set(id, eventSource) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -24,8 +21,8 @@ export class ElementStorage { | |||
|      * Note: it will cleverly merge the tags, if needed | ||||
|      */ | ||||
|     addOrGetElement(feature: any): UIEventSource<any> { | ||||
|         const elementId = feature.properties.id; | ||||
|         const newProperties = feature.properties; | ||||
|         const elementId = feature.properties.id | ||||
|         const newProperties = feature.properties | ||||
| 
 | ||||
|         const es = this.addOrGetById(elementId, newProperties) | ||||
| 
 | ||||
|  | @ -33,91 +30,89 @@ export class ElementStorage { | |||
|         feature.properties = es.data | ||||
| 
 | ||||
|         if (!this.ContainingFeatures.has(elementId)) { | ||||
|             this.ContainingFeatures.set(elementId, feature); | ||||
|             this.ContainingFeatures.set(elementId, feature) | ||||
|         } | ||||
| 
 | ||||
|         return es; | ||||
|         return es | ||||
|     } | ||||
| 
 | ||||
|     getEventSourceById(elementId): UIEventSource<any> { | ||||
|         if (elementId === undefined) { | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
|         return this._elements.get(elementId); | ||||
|         return this._elements.get(elementId) | ||||
|     } | ||||
| 
 | ||||
|     has(id) { | ||||
|         return this._elements.has(id); | ||||
|         return this._elements.has(id) | ||||
|     } | ||||
| 
 | ||||
|     addAlias(oldId: string, newId: string){ | ||||
|     addAlias(oldId: string, newId: string) { | ||||
|         if (newId === undefined) { | ||||
|             // We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
 | ||||
|             const element = this.getEventSourceById(oldId); | ||||
|             const element = this.getEventSourceById(oldId) | ||||
|             element.data._deleted = "yes" | ||||
|             element.ping(); | ||||
|             return; | ||||
|             element.ping() | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (oldId == newId) { | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
|         const element = this.getEventSourceById( oldId); | ||||
|         const element = this.getEventSourceById(oldId) | ||||
|         if (element === undefined) { | ||||
|             // Element to rewrite not found, probably a node or relation that is not rendered
 | ||||
|             return undefined | ||||
|         } | ||||
|         element.data.id = newId; | ||||
|         this.addElementById(newId, element); | ||||
|         this.ContainingFeatures.set(newId, this.ContainingFeatures.get( oldId)) | ||||
|         element.ping(); | ||||
|         element.data.id = newId | ||||
|         this.addElementById(newId, element) | ||||
|         this.ContainingFeatures.set(newId, this.ContainingFeatures.get(oldId)) | ||||
|         element.ping() | ||||
|     } | ||||
| 
 | ||||
|     private addOrGetById(elementId: string, newProperties: any): UIEventSource<any> { | ||||
|         if (!this._elements.has(elementId)) { | ||||
|             const eventSource = new UIEventSource<any>(newProperties, "tags of " + elementId); | ||||
|             this._elements.set(elementId, eventSource); | ||||
|             return eventSource; | ||||
|             const eventSource = new UIEventSource<any>(newProperties, "tags of " + elementId) | ||||
|             this._elements.set(elementId, eventSource) | ||||
|             return eventSource | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const es = this._elements.get(elementId); | ||||
|         const es = this._elements.get(elementId) | ||||
|         if (es.data == newProperties) { | ||||
|             // Reference comparison gives the same object! we can just return the event source
 | ||||
|             return es; | ||||
|             return es | ||||
|         } | ||||
|         const keptKeys = es.data; | ||||
|         const keptKeys = es.data | ||||
|         // The element already exists
 | ||||
|         // We use the new feature to overwrite all the properties in the already existing eventsource
 | ||||
|         const debug_msg = [] | ||||
|         let somethingChanged = false; | ||||
|         let somethingChanged = false | ||||
|         for (const k in newProperties) { | ||||
|             if (!newProperties.hasOwnProperty(k)) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
|             const v = newProperties[k]; | ||||
|             const v = newProperties[k] | ||||
| 
 | ||||
|             if (keptKeys[k] !== v) { | ||||
| 
 | ||||
|                 if (v === undefined) { | ||||
|                     // The new value is undefined; the tag might have been removed
 | ||||
|                     // It might be a metatag as well
 | ||||
|                     // In the latter case, we do keep the tag!
 | ||||
|                     if (!k.startsWith("_")) { | ||||
|                         delete keptKeys[k] | ||||
|                         debug_msg.push(("Erased " + k)) | ||||
|                         debug_msg.push("Erased " + k) | ||||
|                     } | ||||
|                 } else { | ||||
|                     keptKeys[k] = v; | ||||
|                     keptKeys[k] = v | ||||
|                     debug_msg.push(k + " --> " + v) | ||||
|                 } | ||||
| 
 | ||||
|                 somethingChanged = true; | ||||
|                 somethingChanged = true | ||||
|             } | ||||
|         } | ||||
|         if (somethingChanged) { | ||||
|             es.ping(); | ||||
|             es.ping() | ||||
|         } | ||||
|         return es; | ||||
|         return es | ||||
|     } | ||||
| } | ||||
|  | @ -1,11 +1,11 @@ | |||
| import {GeoOperations} from "./GeoOperations"; | ||||
| import Combine from "../UI/Base/Combine"; | ||||
| import RelationsTracker from "./Osm/RelationsTracker"; | ||||
| import BaseUIElement from "../UI/BaseUIElement"; | ||||
| import List from "../UI/Base/List"; | ||||
| import Title from "../UI/Base/Title"; | ||||
| import {BBox} from "./BBox"; | ||||
| import {Feature, Geometry, MultiPolygon, Polygon} from "@turf/turf"; | ||||
| import { GeoOperations } from "./GeoOperations" | ||||
| import Combine from "../UI/Base/Combine" | ||||
| import RelationsTracker from "./Osm/RelationsTracker" | ||||
| import BaseUIElement from "../UI/BaseUIElement" | ||||
| import List from "../UI/Base/List" | ||||
| import Title from "../UI/Base/Title" | ||||
| import { BBox } from "./BBox" | ||||
| import { Feature, Geometry, MultiPolygon, Polygon } from "@turf/turf" | ||||
| 
 | ||||
| export interface ExtraFuncParams { | ||||
|     /** | ||||
|  | @ -13,7 +13,7 @@ export interface ExtraFuncParams { | |||
|      * Note that more features then requested can be given back. | ||||
|      * Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...] | ||||
|      */ | ||||
|     getFeaturesWithin: (layerId: string, bbox: BBox) => Feature<Geometry, { id: string }>[][], | ||||
|     getFeaturesWithin: (layerId: string, bbox: BBox) => Feature<Geometry, { id: string }>[][] | ||||
|     memberships: RelationsTracker | ||||
|     getFeatureById: (id: string) => Feature<Geometry, { id: string }> | ||||
| } | ||||
|  | @ -22,19 +22,23 @@ export interface ExtraFuncParams { | |||
|  * Describes a function that is added to a geojson object in order to calculate calculated tags | ||||
|  */ | ||||
| interface ExtraFunction { | ||||
|     readonly _name: string; | ||||
|     readonly _args: string[]; | ||||
|     readonly _doc: string; | ||||
|     readonly _f: (params: ExtraFuncParams, feat: Feature<Geometry, any>) => any; | ||||
| 
 | ||||
|     readonly _name: string | ||||
|     readonly _args: string[] | ||||
|     readonly _doc: string | ||||
|     readonly _f: (params: ExtraFuncParams, feat: Feature<Geometry, any>) => any | ||||
| } | ||||
| 
 | ||||
| class EnclosingFunc implements ExtraFunction { | ||||
|     _name = "enclosingFeatures" | ||||
|     _doc = ["Gives a list of all features in the specified layers which fully contain this object. Returned features will always be (multi)polygons. (LineStrings and Points from the other layers are ignored)", "", | ||||
|     _doc = [ | ||||
|         "Gives a list of all features in the specified layers which fully contain this object. Returned features will always be (multi)polygons. (LineStrings and Points from the other layers are ignored)", | ||||
|         "", | ||||
|         "The result is a list of features: `{feat: Polygon}[]`", | ||||
|         "This function will never return the feature itself."].join("\n") | ||||
|     _args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"] | ||||
|         "This function will never return the feature itself.", | ||||
|     ].join("\n") | ||||
|     _args = [ | ||||
|         "...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)", | ||||
|     ] | ||||
| 
 | ||||
|     _f(params: ExtraFuncParams, feat: Feature<Geometry, any>) { | ||||
|         return (...layerIds: string[]) => { | ||||
|  | @ -45,10 +49,10 @@ class EnclosingFunc implements ExtraFunction { | |||
|             for (const layerId of layerIds) { | ||||
|                 const otherFeaturess = params.getFeaturesWithin(layerId, bbox) | ||||
|                 if (otherFeaturess === undefined) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
|                 if (otherFeaturess.length === 0) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
|                 for (const otherFeatures of otherFeaturess) { | ||||
|                     for (const otherFeature of otherFeatures) { | ||||
|  | @ -56,26 +60,33 @@ class EnclosingFunc implements ExtraFunction { | |||
|                             continue | ||||
|                         } | ||||
|                         seenIds.add(otherFeature.properties.id) | ||||
|                         if (otherFeature.geometry.type !== "Polygon" && otherFeature.geometry.type !== "MultiPolygon") { | ||||
|                             continue; | ||||
|                         if ( | ||||
|                             otherFeature.geometry.type !== "Polygon" && | ||||
|                             otherFeature.geometry.type !== "MultiPolygon" | ||||
|                         ) { | ||||
|                             continue | ||||
|                         } | ||||
|                         if (GeoOperations.completelyWithin(feat, <Feature<Polygon | MultiPolygon, any>>otherFeature)) { | ||||
|                             result.push({feat: otherFeature}) | ||||
|                         if ( | ||||
|                             GeoOperations.completelyWithin( | ||||
|                                 feat, | ||||
|                                 <Feature<Polygon | MultiPolygon, any>>otherFeature | ||||
|                             ) | ||||
|                         ) { | ||||
|                             result.push({ feat: otherFeature }) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return result; | ||||
|             return result | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class OverlapFunc implements ExtraFunction { | ||||
| 
 | ||||
| 
 | ||||
|     _name = "overlapWith"; | ||||
|     _doc = ["Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well.", | ||||
|     _name = "overlapWith" | ||||
|     _doc = [ | ||||
|         "Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well.", | ||||
|         "If the current feature is a point, all features that this point is embeded in are given.", | ||||
|         "", | ||||
|         "The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.", | ||||
|  | @ -83,27 +94,29 @@ class OverlapFunc implements ExtraFunction { | |||
|         "", | ||||
|         "For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`", | ||||
|         "", | ||||
|         "Also see [enclosingFeatures](#enclosingFeatures) which can be used to get all objects which fully contain this feature" | ||||
|         "Also see [enclosingFeatures](#enclosingFeatures) which can be used to get all objects which fully contain this feature", | ||||
|     ].join("\n") | ||||
|     _args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"] | ||||
|     _args = [ | ||||
|         "...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)", | ||||
|     ] | ||||
| 
 | ||||
|     _f(params, feat) { | ||||
|         return (...layerIds: string[]) => { | ||||
|             const result: { feat: any, overlap: number }[] = [] | ||||
|             const result: { feat: any; overlap: number }[] = [] | ||||
|             const seenIds = new Set<string>() | ||||
|             const bbox = BBox.get(feat) | ||||
|             for (const layerId of layerIds) { | ||||
|                 const otherFeaturess = params.getFeaturesWithin(layerId, bbox) | ||||
|                 if (otherFeaturess === undefined) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
|                 if (otherFeaturess.length === 0) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
|                 for (const otherFeatures of otherFeaturess) { | ||||
|                     const overlap = GeoOperations.calculateOverlap(feat, otherFeatures) | ||||
|                     for (const overlappingFeature of overlap) { | ||||
|                         if(seenIds.has(overlappingFeature.feat.properties.id)){ | ||||
|                         if (seenIds.has(overlappingFeature.feat.properties.id)) { | ||||
|                             continue | ||||
|                         } | ||||
|                         seenIds.add(overlappingFeature.feat.properties.id) | ||||
|  | @ -113,105 +126,113 @@ class OverlapFunc implements ExtraFunction { | |||
|             } | ||||
| 
 | ||||
|             result.sort((a, b) => b.overlap - a.overlap) | ||||
|             return result; | ||||
|             return result | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class IntersectionFunc implements ExtraFunction { | ||||
| 
 | ||||
| 
 | ||||
|     _name = "intersectionsWith"; | ||||
|     _doc = "Gives the intersection points with selected features. Only works with (Multi)Polygons and LineStrings.\n\n" + | ||||
|     _name = "intersectionsWith" | ||||
|     _doc = | ||||
|         "Gives the intersection points with selected features. Only works with (Multi)Polygons and LineStrings.\n\n" + | ||||
|         "Returns a `{feat: GeoJson, intersections: [number,number][]}` where `feat` is the full, original feature. This list is in random order.\n\n" + | ||||
|         "If the current feature is a point, this function will return an empty list.\n" + | ||||
|         "Points from other layers are ignored - even if the points are parts of the current linestring." | ||||
|     _args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for intersection)"] | ||||
|     _args = [ | ||||
|         "...layerIds - one or more layer ids of the layer from which every feature is checked for intersection)", | ||||
|     ] | ||||
| 
 | ||||
|     _f(params: ExtraFuncParams, feat) { | ||||
|         return (...layerIds: string[]) => { | ||||
|             const result: { feat: any, intersections: [number, number][] }[] = [] | ||||
|             const result: { feat: any; intersections: [number, number][] }[] = [] | ||||
| 
 | ||||
|             const bbox = BBox.get(feat) | ||||
| 
 | ||||
|             for (const layerId of layerIds) { | ||||
|                 const otherLayers = params.getFeaturesWithin(layerId, bbox) | ||||
|                 if (otherLayers === undefined) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
|                 if (otherLayers.length === 0) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
|                 for (const tile of otherLayers) { | ||||
|                     for (const otherFeature of tile) { | ||||
| 
 | ||||
|                         const intersections = GeoOperations.LineIntersections(feat, otherFeature) | ||||
|                         if (intersections.length === 0) { | ||||
|                             continue | ||||
|                         } | ||||
|                         result.push({feat: otherFeature, intersections}) | ||||
|                         result.push({ feat: otherFeature, intersections }) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return result; | ||||
|             return result | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class DistanceToFunc implements ExtraFunction { | ||||
| 
 | ||||
|     _name = "distanceTo"; | ||||
|     _doc = "Calculates the distance between the feature and a specified point in meter. The input should either be a pair of coordinates, a geojson feature or the ID of an object"; | ||||
|     _name = "distanceTo" | ||||
|     _doc = | ||||
|         "Calculates the distance between the feature and a specified point in meter. The input should either be a pair of coordinates, a geojson feature or the ID of an object" | ||||
|     _args = ["feature OR featureID OR longitude", "undefined OR latitude"] | ||||
| 
 | ||||
|     _f(featuresPerLayer, feature) { | ||||
|         return (arg0, lat) => { | ||||
|             if (arg0 === undefined) { | ||||
|                 return undefined; | ||||
|                 return undefined | ||||
|             } | ||||
|             if (typeof arg0 === "number") { | ||||
|                 // Feature._lon and ._lat is conveniently place by one of the other metatags
 | ||||
|                 return GeoOperations.distanceBetween([arg0, lat], GeoOperations.centerpointCoordinates(feature)); | ||||
|                 return GeoOperations.distanceBetween( | ||||
|                     [arg0, lat], | ||||
|                     GeoOperations.centerpointCoordinates(feature) | ||||
|                 ) | ||||
|             } | ||||
|             if (typeof arg0 === "string") { | ||||
|                 // This is an identifier
 | ||||
|                 const feature = featuresPerLayer.getFeatureById(arg0) | ||||
|                 if (feature === undefined) { | ||||
|                     return undefined; | ||||
|                     return undefined | ||||
|                 } | ||||
|                 arg0 = feature; | ||||
|                 arg0 = feature | ||||
|             } | ||||
| 
 | ||||
|             // arg0 is probably a geojsonfeature
 | ||||
|             return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0), GeoOperations.centerpointCoordinates(feature)) | ||||
| 
 | ||||
|             return GeoOperations.distanceBetween( | ||||
|                 GeoOperations.centerpointCoordinates(arg0), | ||||
|                 GeoOperations.centerpointCoordinates(feature) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class ClosestObjectFunc implements ExtraFunction { | ||||
|     _name = "closest" | ||||
|     _doc = "Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. Returns a single geojson feature or undefined if nothing is found (or not yet loaded)" | ||||
|     _doc = | ||||
|         "Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. Returns a single geojson feature or undefined if nothing is found (or not yet loaded)" | ||||
| 
 | ||||
|     _args = ["list of features or a layer name or '*' to get all features"] | ||||
| 
 | ||||
|     _f(params, feature) { | ||||
|         return (features) => ClosestNObjectFunc.GetClosestNFeatures(params, feature, features)?.[0]?.feat | ||||
|         return (features) => | ||||
|             ClosestNObjectFunc.GetClosestNFeatures(params, feature, features)?.[0]?.feat | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class ClosestNObjectFunc implements ExtraFunction { | ||||
|     _name = "closestn" | ||||
|     _doc = "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature (excluding the feature itself). In the case of ways/polygons, only the centerpoint is considered. " + | ||||
|     _doc = | ||||
|         "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature (excluding the feature itself). In the case of ways/polygons, only the centerpoint is considered. " + | ||||
|         "Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet loaded)\n\n" + | ||||
|         "If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)" | ||||
|     _args = ["list of features or layer name or '*' to get all features", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"] | ||||
|     _args = [ | ||||
|         "list of features or layer name or '*' to get all features", | ||||
|         "amount of features", | ||||
|         "unique tag key (optional)", | ||||
|         "maxDistanceInMeters (optional)", | ||||
|     ] | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the closes N features, sorted by ascending distance. | ||||
|  | @ -223,45 +244,61 @@ class ClosestNObjectFunc implements ExtraFunction { | |||
|      * @constructor | ||||
|      * @private | ||||
|      */ | ||||
|     static GetClosestNFeatures(params: ExtraFuncParams, | ||||
|     static GetClosestNFeatures( | ||||
|         params: ExtraFuncParams, | ||||
|         feature: any, | ||||
|         features: string | any[], | ||||
|                                options?: { maxFeatures?: number, uniqueTag?: string | undefined, maxDistance?: number }): { feat: any, distance: number }[] { | ||||
|         options?: { maxFeatures?: number; uniqueTag?: string | undefined; maxDistance?: number } | ||||
|     ): { feat: any; distance: number }[] { | ||||
|         const maxFeatures = options?.maxFeatures ?? 1 | ||||
|         const maxDistance = options?.maxDistance ?? 500 | ||||
|         const uniqueTag: string | undefined = options?.uniqueTag | ||||
|         if (typeof features === "string") { | ||||
|             const name = features | ||||
|             const bbox = GeoOperations.bbox(GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance)) | ||||
|             const bbox = GeoOperations.bbox( | ||||
|                 GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance) | ||||
|             ) | ||||
|             features = params.getFeaturesWithin(name, new BBox(bbox.geometry.coordinates)) | ||||
|         } else { | ||||
|             features = [features] | ||||
|         } | ||||
|         if (features === undefined) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         const selfCenter = GeoOperations.centerpointCoordinates(feature) | ||||
|         let closestFeatures: { feat: any, distance: number }[] = []; | ||||
|         let closestFeatures: { feat: any; distance: number }[] = [] | ||||
| 
 | ||||
|         for (const featureList of features) { | ||||
|             // Features is provided by 'getFeaturesWithin' which returns a list of lists of features, hence the double loop here
 | ||||
|             for (const otherFeature of featureList) { | ||||
| 
 | ||||
|                 if (otherFeature === feature || otherFeature.properties.id === feature.properties.id) { | ||||
|                     continue; // We ignore self
 | ||||
|                 if ( | ||||
|                     otherFeature === feature || | ||||
|                     otherFeature.properties.id === feature.properties.id | ||||
|                 ) { | ||||
|                     continue // We ignore self
 | ||||
|                 } | ||||
|                 const distance = GeoOperations.distanceBetween( | ||||
|                     GeoOperations.centerpointCoordinates(otherFeature), | ||||
|                     selfCenter | ||||
|                 ) | ||||
|                 if (distance === undefined || distance === null || isNaN(distance)) { | ||||
|                     console.error("Could not calculate the distance between", feature, "and", otherFeature) | ||||
|                     console.error( | ||||
|                         "Could not calculate the distance between", | ||||
|                         feature, | ||||
|                         "and", | ||||
|                         otherFeature | ||||
|                     ) | ||||
|                     throw "Undefined distance!" | ||||
|                 } | ||||
| 
 | ||||
|                 if (distance === 0) { | ||||
|                     console.trace("Got a suspiciously zero distance between", otherFeature, "and self-feature", feature) | ||||
|                     console.trace( | ||||
|                         "Got a suspiciously zero distance between", | ||||
|                         otherFeature, | ||||
|                         "and self-feature", | ||||
|                         feature | ||||
|                     ) | ||||
|                 } | ||||
| 
 | ||||
|                 if (distance > maxDistance) { | ||||
|  | @ -272,13 +309,15 @@ class ClosestNObjectFunc implements ExtraFunction { | |||
|                     // This is the first matching feature we find - always add it
 | ||||
|                     closestFeatures.push({ | ||||
|                         feat: otherFeature, | ||||
|                         distance: distance | ||||
|                         distance: distance, | ||||
|                     }) | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { | ||||
|                 if ( | ||||
|                     closestFeatures.length >= maxFeatures && | ||||
|                     closestFeatures[maxFeatures - 1].distance < distance | ||||
|                 ) { | ||||
|                     // The last feature of the list (and thus the furthest away is still closer
 | ||||
|                     // No use for checking, as we already have plenty of features!
 | ||||
|                     continue | ||||
|  | @ -286,11 +325,13 @@ class ClosestNObjectFunc implements ExtraFunction { | |||
| 
 | ||||
|                 let targetIndex = closestFeatures.length | ||||
|                 for (let i = 0; i < closestFeatures.length; i++) { | ||||
|                     const closestFeature = closestFeatures[i]; | ||||
|                     const closestFeature = closestFeatures[i] | ||||
| 
 | ||||
|                     if (uniqueTag !== undefined) { | ||||
|                         const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined && | ||||
|                             closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag] | ||||
|                         const uniqueTagsMatch = | ||||
|                             otherFeature.properties[uniqueTag] !== undefined && | ||||
|                             closestFeature.feat.properties[uniqueTag] === | ||||
|                                 otherFeature.properties[uniqueTag] | ||||
|                         if (uniqueTagsMatch) { | ||||
|                             targetIndex = -1 | ||||
|                             if (closestFeature.distance > distance) { | ||||
|  | @ -298,9 +339,9 @@ class ClosestNObjectFunc implements ExtraFunction { | |||
|                                 // We want to see the tag `uniquetag=some_value` only once in the entire list (e.g. to prevent road segements of identical names to fill up the list of 'names of nearby roads')
 | ||||
|                                 // AT this point, we have found a closer segment with the same, identical tag
 | ||||
|                                 // so we replace directly
 | ||||
|                                 closestFeatures[i] = {feat: otherFeature, distance: distance} | ||||
|                                 closestFeatures[i] = { feat: otherFeature, distance: distance } | ||||
|                             } | ||||
|                             break; | ||||
|                             break | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|  | @ -316,19 +357,19 @@ class ClosestNObjectFunc implements ExtraFunction { | |||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         break; | ||||
|                         break | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 if (targetIndex == -1) { | ||||
|                     continue; // value is already swapped by the unique tag
 | ||||
|                     continue // value is already swapped by the unique tag
 | ||||
|                 } | ||||
| 
 | ||||
|                 if (targetIndex < maxFeatures) { | ||||
|                     // insert and drop one
 | ||||
|                     closestFeatures.splice(targetIndex, 0, { | ||||
|                         feat: otherFeature, | ||||
|                         distance: distance | ||||
|                         distance: distance, | ||||
|                     }) | ||||
|                     if (closestFeatures.length >= maxFeatures) { | ||||
|                         closestFeatures.splice(maxFeatures, 1) | ||||
|  | @ -337,19 +378,15 @@ class ClosestNObjectFunc implements ExtraFunction { | |||
|                     // Overwrite the last element
 | ||||
|                     closestFeatures[targetIndex] = { | ||||
|                         feat: otherFeature, | ||||
|                         distance: distance | ||||
|                     } | ||||
| 
 | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                         distance: distance, | ||||
|                     } | ||||
|                 } | ||||
|         return closestFeatures; | ||||
|             } | ||||
|         } | ||||
|         return closestFeatures | ||||
|     } | ||||
| 
 | ||||
|     _f(params, feature) { | ||||
| 
 | ||||
|         return (features, amount, uniqueTag, maxDistanceInMeters) => { | ||||
|             let distance: number = Number(maxDistanceInMeters) | ||||
|             if (isNaN(distance)) { | ||||
|  | @ -358,60 +395,54 @@ class ClosestNObjectFunc implements ExtraFunction { | |||
|             return ClosestNObjectFunc.GetClosestNFeatures(params, feature, features, { | ||||
|                 maxFeatures: Number(amount), | ||||
|                 uniqueTag: uniqueTag, | ||||
|                 maxDistance: distance | ||||
|             }); | ||||
|                 maxDistance: distance, | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class Memberships implements ExtraFunction { | ||||
|     _name = "memberships" | ||||
|     _doc = "Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of. " + | ||||
|     _doc = | ||||
|         "Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of. " + | ||||
|         "\n\n" + | ||||
|         "For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`" | ||||
|     _args = [] | ||||
| 
 | ||||
|     _f(params, feat) { | ||||
|         return () => | ||||
|             params.memberships.knownRelations.data.get(feat.properties.id) ?? [] | ||||
| 
 | ||||
|         return () => params.memberships.knownRelations.data.get(feat.properties.id) ?? [] | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class GetParsed implements ExtraFunction { | ||||
|     _name = "get" | ||||
|     _doc = "Gets the property of the feature, parses it (as JSON) and returns it. Might return 'undefined' if not defined, null, ..." | ||||
|     _doc = | ||||
|         "Gets the property of the feature, parses it (as JSON) and returns it. Might return 'undefined' if not defined, null, ..." | ||||
|     _args = ["key"] | ||||
| 
 | ||||
|     _f(params, feat) { | ||||
|         return key => { | ||||
|         return (key) => { | ||||
|             const value = feat.properties[key] | ||||
|             if (value === undefined) { | ||||
|                 return undefined; | ||||
|                 return undefined | ||||
|             } | ||||
|             try { | ||||
|                 const parsed = JSON.parse(value) | ||||
|                 if (parsed === null) { | ||||
|                     return undefined; | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return parsed; | ||||
|                 return parsed | ||||
|             } catch (e) { | ||||
|                 console.warn("Could not parse property " + key + " due to: " + e + ", the value is " + value) | ||||
|                 return undefined; | ||||
|                 console.warn( | ||||
|                     "Could not parse property " + key + " due to: " + e + ", the value is " + value | ||||
|                 ) | ||||
|                 return undefined | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export class ExtraFunctions { | ||||
| 
 | ||||
| 
 | ||||
|     static readonly intro = new Combine([ | ||||
|         new Title("Calculating tags with Javascript", 2), | ||||
|         "In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. `lat`, `lon`, `_country`), as detailed above.", | ||||
|  | @ -421,13 +452,13 @@ export class ExtraFunctions { | |||
|         new List([ | ||||
|             "DO NOT DO THIS AS BEGINNER", | ||||
|             "**Only do this if all other techniques fail**  This should _not_ be done to create a rendering effect, only to calculate a specific value", | ||||
|             "**THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs." | ||||
|             "**THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs.", | ||||
|         ]), | ||||
|         "To enable this feature,  add a field `calculatedTags` in the layer object, e.g.:", | ||||
|         "````", | ||||
|         "\"calculatedTags\": [", | ||||
|         "    \"_someKey=javascript-expression\",", | ||||
|         "    \"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator\",", | ||||
|         '"calculatedTags": [', | ||||
|         '    "_someKey=javascript-expression",', | ||||
|         '    "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",', | ||||
|         "    \"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ", | ||||
|         "  ]", | ||||
|         "````", | ||||
|  | @ -436,11 +467,12 @@ export class ExtraFunctions { | |||
| 
 | ||||
|         new List([ | ||||
|             "`area` contains the surface area (in square meters) of the object", | ||||
|             "`lat` and `lon` contain the latitude and longitude" | ||||
|             "`lat` and `lon` contain the latitude and longitude", | ||||
|         ]), | ||||
|         "Some advanced functions are available on **feat** as well:" | ||||
|     ]).SetClass("flex-col").AsMarkdown(); | ||||
| 
 | ||||
|         "Some advanced functions are available on **feat** as well:", | ||||
|     ]) | ||||
|         .SetClass("flex-col") | ||||
|         .AsMarkdown() | ||||
| 
 | ||||
|     private static readonly allFuncs: ExtraFunction[] = [ | ||||
|         new DistanceToFunc(), | ||||
|  | @ -450,8 +482,8 @@ export class ExtraFunctions { | |||
|         new ClosestObjectFunc(), | ||||
|         new ClosestNObjectFunc(), | ||||
|         new Memberships(), | ||||
|         new GetParsed() | ||||
|     ]; | ||||
|         new GetParsed(), | ||||
|     ] | ||||
| 
 | ||||
|     public static FullPatchFeature(params: ExtraFuncParams, feature) { | ||||
|         if (feature._is_patched) { | ||||
|  | @ -464,20 +496,15 @@ export class ExtraFunctions { | |||
|     } | ||||
| 
 | ||||
|     public static HelpText(): BaseUIElement { | ||||
| 
 | ||||
|         const elems = [] | ||||
|         for (const func of ExtraFunctions.allFuncs) { | ||||
|             elems.push(new Title(func._name, 3), | ||||
|                 func._doc, | ||||
|                 new List(func._args ?? [], true)) | ||||
|             elems.push(new Title(func._name, 3), func._doc, new List(func._args ?? [], true)) | ||||
|         } | ||||
| 
 | ||||
|         return new Combine([ | ||||
|             ExtraFunctions.intro, | ||||
|             new List(ExtraFunctions.allFuncs.map(func => `[${func._name}](#${func._name})`)), | ||||
|             ...elems | ||||
|         ]); | ||||
|             new List(ExtraFunctions.allFuncs.map((func) => `[${func._name}](#${func._name})`)), | ||||
|             ...elems, | ||||
|         ]) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,26 +1,30 @@ | |||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import MetaTagging from "../../MetaTagging"; | ||||
| import {ElementStorage} from "../../ElementStorage"; | ||||
| import {ExtraFuncParams} from "../../ExtraFunctions"; | ||||
| import FeaturePipeline from "../FeaturePipeline"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import MetaTagging from "../../MetaTagging" | ||||
| import { ElementStorage } from "../../ElementStorage" | ||||
| import { ExtraFuncParams } from "../../ExtraFunctions" | ||||
| import FeaturePipeline from "../FeaturePipeline" | ||||
| import { BBox } from "../../BBox" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| 
 | ||||
| /**** | ||||
|  * Concerned with the logic of updating the right layer at the right time | ||||
|  */ | ||||
| class MetatagUpdater { | ||||
|     public readonly neededLayerBboxes = new Map<string /*layerId*/, BBox>() | ||||
|     private source: FeatureSourceForLayer & Tiled; | ||||
|     private source: FeatureSourceForLayer & Tiled | ||||
|     private readonly params: ExtraFuncParams | ||||
|     private state: { allElements?: ElementStorage }; | ||||
|     private state: { allElements?: ElementStorage } | ||||
| 
 | ||||
|     private readonly isDirty = new UIEventSource(false) | ||||
| 
 | ||||
|     constructor(source: FeatureSourceForLayer & Tiled, state: { allElements?: ElementStorage }, featurePipeline: FeaturePipeline) { | ||||
|         this.state = state; | ||||
|         this.source = source; | ||||
|         const self = this; | ||||
|     constructor( | ||||
|         source: FeatureSourceForLayer & Tiled, | ||||
|         state: { allElements?: ElementStorage }, | ||||
|         featurePipeline: FeaturePipeline | ||||
|     ) { | ||||
|         this.state = state | ||||
|         this.source = source | ||||
|         const self = this | ||||
|         this.params = { | ||||
|             getFeatureById(id) { | ||||
|                 return state.allElements.ContainingFeatures.get(id) | ||||
|  | @ -29,21 +33,20 @@ class MetatagUpdater { | |||
|                 // We keep track of the BBOX that this source needs
 | ||||
|                 let oldBbox: BBox = self.neededLayerBboxes.get(layerId) | ||||
|                 if (oldBbox === undefined) { | ||||
|                     self.neededLayerBboxes.set(layerId, bbox); | ||||
|                     self.neededLayerBboxes.set(layerId, bbox) | ||||
|                 } else if (!bbox.isContainedIn(oldBbox)) { | ||||
|                     self.neededLayerBboxes.set(layerId, oldBbox.unionWith(bbox)) | ||||
|                 } | ||||
|                 return featurePipeline.GetFeaturesWithin(layerId, bbox) | ||||
|             }, | ||||
|             memberships: featurePipeline.relationTracker | ||||
|             memberships: featurePipeline.relationTracker, | ||||
|         } | ||||
|         this.isDirty.stabilized(100).addCallback(dirty => { | ||||
|         this.isDirty.stabilized(100).addCallback((dirty) => { | ||||
|             if (dirty) { | ||||
|                 self.updateMetaTags() | ||||
|             } | ||||
|         }) | ||||
|         this.source.features.addCallbackAndRunD(_ => self.isDirty.setData(true)) | ||||
| 
 | ||||
|         this.source.features.addCallbackAndRunD((_) => self.isDirty.setData(true)) | ||||
|     } | ||||
| 
 | ||||
|     public requestUpdate() { | ||||
|  | @ -57,56 +60,58 @@ class MetatagUpdater { | |||
|             this.isDirty.setData(false) | ||||
|             return | ||||
|         } | ||||
|         MetaTagging.addMetatags( | ||||
|             features, | ||||
|             this.params, | ||||
|             this.source.layer.layerDef, | ||||
|             this.state) | ||||
|         MetaTagging.addMetatags(features, this.params, this.source.layer.layerDef, this.state) | ||||
|         this.isDirty.setData(false) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export default class MetaTagRecalculator { | ||||
|     private _state: { | ||||
|         allElements?: ElementStorage | ||||
|     }; | ||||
|     private _featurePipeline: FeaturePipeline; | ||||
|     private readonly _alreadyRegistered: Set<FeatureSourceForLayer & Tiled> = new Set<FeatureSourceForLayer & Tiled>() | ||||
|     } | ||||
|     private _featurePipeline: FeaturePipeline | ||||
|     private readonly _alreadyRegistered: Set<FeatureSourceForLayer & Tiled> = new Set< | ||||
|         FeatureSourceForLayer & Tiled | ||||
|     >() | ||||
|     private readonly _notifiers: MetatagUpdater[] = [] | ||||
| 
 | ||||
|     /** | ||||
|      * The meta tag recalculator receives tiles of layers via the 'registerSource'-function. | ||||
|      * It keeps track of which sources have had their share calculated, and which should be re-updated if some other data is loaded | ||||
|      */ | ||||
|     constructor(state: { allElements?: ElementStorage, currentView: FeatureSourceForLayer & Tiled }, featurePipeline: FeaturePipeline) { | ||||
|         this._featurePipeline = featurePipeline; | ||||
|         this._state = state; | ||||
|     constructor( | ||||
|         state: { allElements?: ElementStorage; currentView: FeatureSourceForLayer & Tiled }, | ||||
|         featurePipeline: FeaturePipeline | ||||
|     ) { | ||||
|         this._featurePipeline = featurePipeline | ||||
|         this._state = state | ||||
| 
 | ||||
|         if(state.currentView !== undefined){ | ||||
|         const currentViewUpdater = new MetatagUpdater(state.currentView, this._state, this._featurePipeline) | ||||
|         if (state.currentView !== undefined) { | ||||
|             const currentViewUpdater = new MetatagUpdater( | ||||
|                 state.currentView, | ||||
|                 this._state, | ||||
|                 this._featurePipeline | ||||
|             ) | ||||
|             this._alreadyRegistered.add(state.currentView) | ||||
|             this._notifiers.push(currentViewUpdater) | ||||
|         state.currentView.features.addCallback(_ => { | ||||
|             state.currentView.features.addCallback((_) => { | ||||
|                 console.debug("Requesting an update for currentView") | ||||
|             currentViewUpdater.updateMetaTags(); | ||||
|                 currentViewUpdater.updateMetaTags() | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public registerSource(source: FeatureSourceForLayer & Tiled, recalculateOnEveryChange = false) { | ||||
|         if (source === undefined) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         if (this._alreadyRegistered.has(source)) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         this._alreadyRegistered.add(source) | ||||
|         this._notifiers.push(new MetatagUpdater(source, this._state, this._featurePipeline)) | ||||
|         const self = this; | ||||
|         source.features.addCallbackAndRunD(_ => { | ||||
|         const self = this | ||||
|         source.features.addCallbackAndRunD((_) => { | ||||
|             const layerName = source.layer.layerDef.id | ||||
|             for (const updater of self._notifiers) { | ||||
|                 const neededBbox = updater.neededLayerBboxes.get(layerName) | ||||
|  | @ -118,7 +123,5 @@ export default class MetaTagRecalculator { | |||
|                 } | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,22 +1,21 @@ | |||
| import FeatureSource from "../FeatureSource"; | ||||
| import {Store} from "../../UIEventSource"; | ||||
| import {ElementStorage} from "../../ElementStorage"; | ||||
| import FeatureSource from "../FeatureSource" | ||||
| import { Store } from "../../UIEventSource" | ||||
| import { ElementStorage } from "../../ElementStorage" | ||||
| 
 | ||||
| /** | ||||
|  * Makes sure that every feature is added to the ElementsStorage, so that the tags-eventsource can be retrieved | ||||
|  */ | ||||
| export default class RegisteringAllFromFeatureSourceActor { | ||||
|     public readonly features: Store<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly name; | ||||
|     public readonly features: Store<{ feature: any; freshness: Date }[]> | ||||
|     public readonly name | ||||
| 
 | ||||
|     constructor(source: FeatureSource, allElements: ElementStorage) { | ||||
|         this.features = source.features; | ||||
|         this.name = "RegisteringSource of " + source.name; | ||||
|         this.features.addCallbackAndRunD(features => { | ||||
|         this.features = source.features | ||||
|         this.name = "RegisteringSource of " + source.name | ||||
|         this.features.addCallbackAndRunD((features) => { | ||||
|             for (const feature of features) { | ||||
|                 allElements.addOrGetElement(feature.feature) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,12 +1,12 @@ | |||
| import FeatureSource, {Tiled} from "../FeatureSource"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {IdbLocalStorage} from "../../Web/IdbLocalStorage"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import SimpleFeatureSource from "../Sources/SimpleFeatureSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import Loc from "../../../Models/Loc"; | ||||
| import FeatureSource, { Tiled } from "../FeatureSource" | ||||
| import { Tiles } from "../../../Models/TileRange" | ||||
| import { IdbLocalStorage } from "../../Web/IdbLocalStorage" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||
| import { BBox } from "../../BBox" | ||||
| import SimpleFeatureSource from "../Sources/SimpleFeatureSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import Loc from "../../../Models/Loc" | ||||
| 
 | ||||
| /*** | ||||
|  * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run | ||||
|  | @ -15,20 +15,23 @@ import Loc from "../../../Models/Loc"; | |||
|  */ | ||||
| export default class SaveTileToLocalStorageActor { | ||||
|     private readonly visitedTiles: UIEventSource<Map<number, Date>> | ||||
|     private readonly _layer: LayerConfig; | ||||
|     private readonly _layer: LayerConfig | ||||
|     private readonly _flayer: FilteredLayer | ||||
|     private readonly initializeTime = new Date() | ||||
| 
 | ||||
|     constructor(layer: FilteredLayer) { | ||||
|         this._flayer = layer | ||||
|         this._layer = layer.layerDef | ||||
|         this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, | ||||
|             {defaultValue: new Map<number, Date>(),}) | ||||
|         this.visitedTiles.stabilized(100).addCallbackAndRunD(tiles => { | ||||
|         this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, { | ||||
|             defaultValue: new Map<number, Date>(), | ||||
|         }) | ||||
|         this.visitedTiles.stabilized(100).addCallbackAndRunD((tiles) => { | ||||
|             for (const key of Array.from(tiles.keys())) { | ||||
|                 const tileFreshness = tiles.get(key) | ||||
| 
 | ||||
|                 const toOld = (this.initializeTime.getTime() - tileFreshness.getTime()) > 1000 * this._layer.maxAgeOfCache | ||||
|                 const toOld = | ||||
|                     this.initializeTime.getTime() - tileFreshness.getTime() > | ||||
|                     1000 * this._layer.maxAgeOfCache | ||||
|                 if (toOld) { | ||||
|                     // Purge this tile
 | ||||
|                     this.SetIdb(key, undefined) | ||||
|  | @ -37,27 +40,28 @@ export default class SaveTileToLocalStorageActor { | |||
|                 } | ||||
|             } | ||||
|             this.visitedTiles.ping() | ||||
|             return true; | ||||
|             return true | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public LoadTilesFromDisk(currentBounds: UIEventSource<BBox>, location: UIEventSource<Loc>, | ||||
|     public LoadTilesFromDisk( | ||||
|         currentBounds: UIEventSource<BBox>, | ||||
|         location: UIEventSource<Loc>, | ||||
|         registerFreshness: (tileId: number, freshness: Date) => void, | ||||
|                              registerTile: ((src: FeatureSource & Tiled) => void)) { | ||||
|         const self = this; | ||||
|         registerTile: (src: FeatureSource & Tiled) => void | ||||
|     ) { | ||||
|         const self = this | ||||
|         const loadedTiles = new Set<number>() | ||||
|         this.visitedTiles.addCallbackD(tiles => { | ||||
|         this.visitedTiles.addCallbackD((tiles) => { | ||||
|             if (tiles.size === 0) { | ||||
|                 // We don't do anything yet as probably not yet loaded from disk
 | ||||
|                 // We'll unregister later on
 | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             currentBounds.addCallbackAndRunD(bbox => { | ||||
| 
 | ||||
|             currentBounds.addCallbackAndRunD((bbox) => { | ||||
|                 if (self._layer.minzoomVisible > location.data.zoom) { | ||||
|                     // Not enough zoom
 | ||||
|                     return; | ||||
|                     return | ||||
|                 } | ||||
| 
 | ||||
|                 // Iterate over all available keys in the local storage, check which are needed and fresh enough
 | ||||
|  | @ -71,32 +75,35 @@ export default class SaveTileToLocalStorageActor { | |||
|                     registerFreshness(key, tileFreshness) | ||||
|                     const tileBbox = BBox.fromTileIndex(key) | ||||
|                     if (!bbox.overlapsWith(tileBbox)) { | ||||
|                         continue; | ||||
|                         continue | ||||
|                     } | ||||
|                     if (loadedTiles.has(key)) { | ||||
|                         // Already loaded earlier
 | ||||
|                         continue | ||||
|                     } | ||||
|                     loadedTiles.add(key) | ||||
|                     this.GetIdb(key).then((features: { feature: any, freshness: Date }[]) => { | ||||
|                         if(features === undefined){ | ||||
|                             return; | ||||
|                     this.GetIdb(key).then((features: { feature: any; freshness: Date }[]) => { | ||||
|                         if (features === undefined) { | ||||
|                             return | ||||
|                         } | ||||
|                         console.debug("Loaded tile " + self._layer.id + "_" + key + " from disk") | ||||
|                         const src = new SimpleFeatureSource(self._flayer, key, new UIEventSource<{ feature: any; freshness: Date }[]>(features)) | ||||
|                         const src = new SimpleFeatureSource( | ||||
|                             self._flayer, | ||||
|                             key, | ||||
|                             new UIEventSource<{ feature: any; freshness: Date }[]>(features) | ||||
|                         ) | ||||
|                         registerTile(src) | ||||
|                     }) | ||||
|                 } | ||||
|             }) | ||||
| 
 | ||||
|             return true; // Remove the callback
 | ||||
| 
 | ||||
|             return true // Remove the callback
 | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public addTile(tile: FeatureSource & Tiled) { | ||||
|         const self = this | ||||
|         tile.features.addCallbackAndRunD(features => { | ||||
|         tile.features.addCallbackAndRunD((features) => { | ||||
|             const now = new Date() | ||||
| 
 | ||||
|             if (features.length > 0) { | ||||
|  | @ -109,11 +116,10 @@ export default class SaveTileToLocalStorageActor { | |||
| 
 | ||||
|     public poison(lon: number, lat: number) { | ||||
|         for (let z = 0; z < 25; z++) { | ||||
|             const {x, y} = Tiles.embedded_tile(lat, lon, z) | ||||
|             const { x, y } = Tiles.embedded_tile(lat, lon, z) | ||||
|             const tileId = Tiles.tile_index(z, x, y) | ||||
|             this.visitedTiles.data.delete(tileId) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public MarkVisited(tileId: number, freshness: Date) { | ||||
|  | @ -125,7 +131,14 @@ export default class SaveTileToLocalStorageActor { | |||
|         try { | ||||
|             IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data) | ||||
|         } catch (e) { | ||||
|             console.error("Could not save tile to indexed-db: ", e, "tileIndex is:", tileIndex, "for layer", this._layer.id) | ||||
|             console.error( | ||||
|                 "Could not save tile to indexed-db: ", | ||||
|                 e, | ||||
|                 "tileIndex is:", | ||||
|                 tileIndex, | ||||
|                 "for layer", | ||||
|                 this._layer.id | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,34 +1,33 @@ | |||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import FilteringFeatureSource from "./Sources/FilteringFeatureSource"; | ||||
| import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter"; | ||||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "./FeatureSource"; | ||||
| import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource"; | ||||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy"; | ||||
| import RememberingSource from "./Sources/RememberingSource"; | ||||
| import OverpassFeatureSource from "../Actors/OverpassFeatureSource"; | ||||
| import GeoJsonSource from "./Sources/GeoJsonSource"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor"; | ||||
| import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor"; | ||||
| import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource"; | ||||
| import {TileHierarchyMerger} from "./TiledFeatureSource/TileHierarchyMerger"; | ||||
| import RelationsTracker from "../Osm/RelationsTracker"; | ||||
| import {NewGeometryFromChangesFeatureSource} from "./Sources/NewGeometryFromChangesFeatureSource"; | ||||
| import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator"; | ||||
| import {BBox} from "../BBox"; | ||||
| import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource"; | ||||
| import {Tiles} from "../../Models/TileRange"; | ||||
| import TileFreshnessCalculator from "./TileFreshnessCalculator"; | ||||
| import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource"; | ||||
| import MapState from "../State/MapState"; | ||||
| import {ElementStorage} from "../ElementStorage"; | ||||
| import {OsmFeature} from "../../Models/OsmFeature"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import {FilterState} from "../../Models/FilteredLayer"; | ||||
| import {GeoOperations} from "../GeoOperations"; | ||||
| import {Utils} from "../../Utils"; | ||||
| 
 | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import FilteringFeatureSource from "./Sources/FilteringFeatureSource" | ||||
| import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter" | ||||
| import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "./FeatureSource" | ||||
| import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource" | ||||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import { TileHierarchyTools } from "./TiledFeatureSource/TileHierarchy" | ||||
| import RememberingSource from "./Sources/RememberingSource" | ||||
| import OverpassFeatureSource from "../Actors/OverpassFeatureSource" | ||||
| import GeoJsonSource from "./Sources/GeoJsonSource" | ||||
| import Loc from "../../Models/Loc" | ||||
| import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor" | ||||
| import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor" | ||||
| import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource" | ||||
| import { TileHierarchyMerger } from "./TiledFeatureSource/TileHierarchyMerger" | ||||
| import RelationsTracker from "../Osm/RelationsTracker" | ||||
| import { NewGeometryFromChangesFeatureSource } from "./Sources/NewGeometryFromChangesFeatureSource" | ||||
| import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator" | ||||
| import { BBox } from "../BBox" | ||||
| import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource" | ||||
| import { Tiles } from "../../Models/TileRange" | ||||
| import TileFreshnessCalculator from "./TileFreshnessCalculator" | ||||
| import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource" | ||||
| import MapState from "../State/MapState" | ||||
| import { ElementStorage } from "../ElementStorage" | ||||
| import { OsmFeature } from "../../Models/OsmFeature" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import { FilterState } from "../../Models/FilteredLayer" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| /** | ||||
|  * The features pipeline ties together a myriad of various datasources: | ||||
|  | @ -42,12 +41,12 @@ import {Utils} from "../../Utils"; | |||
|  * | ||||
|  */ | ||||
| export default class FeaturePipeline { | ||||
| 
 | ||||
|     public readonly sufficientlyZoomed: Store<boolean>; | ||||
|     public readonly runningQuery: Store<boolean>; | ||||
|     public readonly timeout: UIEventSource<number>; | ||||
|     public readonly sufficientlyZoomed: Store<boolean> | ||||
|     public readonly runningQuery: Store<boolean> | ||||
|     public readonly timeout: UIEventSource<number> | ||||
|     public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
|     public readonly newDataLoadedSignal: UIEventSource<FeatureSource> = new UIEventSource<FeatureSource>(undefined) | ||||
|     public readonly newDataLoadedSignal: UIEventSource<FeatureSource> = | ||||
|         new UIEventSource<FeatureSource>(undefined) | ||||
|     public readonly relationTracker: RelationsTracker | ||||
|     /** | ||||
|      * Keeps track of all raw OSM-nodes. | ||||
|  | @ -55,19 +54,19 @@ export default class FeaturePipeline { | |||
|      */ | ||||
|     public readonly fullNodeDatabase?: FullNodeDatabaseSource | ||||
|     private readonly overpassUpdater: OverpassFeatureSource | ||||
|     private state: MapState; | ||||
|     private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>; | ||||
|     private state: MapState | ||||
|     private readonly perLayerHierarchy: Map<string, TileHierarchyMerger> | ||||
|     /** | ||||
|      * Keeps track of the age of the loaded data. | ||||
|      * Has one freshness-Calculator for every layer | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly freshnesses = new Map<string, TileFreshnessCalculator>(); | ||||
|     private readonly oldestAllowedDate: Date; | ||||
|     private readonly freshnesses = new Map<string, TileFreshnessCalculator>() | ||||
|     private readonly oldestAllowedDate: Date | ||||
|     private readonly osmSourceZoomLevel | ||||
|     private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>() | ||||
| 
 | ||||
|     private readonly newGeometryHandler : NewGeometryFromChangesFeatureSource; | ||||
|     private readonly newGeometryHandler: NewGeometryFromChangesFeatureSource | ||||
| 
 | ||||
|     constructor( | ||||
|         handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, | ||||
|  | @ -77,33 +76,40 @@ export default class FeaturePipeline { | |||
|             handleRawFeatureSource: (source: FeatureSourceForLayer) => void | ||||
|         } | ||||
|     ) { | ||||
|         this.state = state; | ||||
|         this.state = state | ||||
| 
 | ||||
|         const self = this | ||||
|         const expiryInSeconds = Math.min(...state.layoutToUse?.layers?.map(l => l.maxAgeOfCache) ?? []) | ||||
|         this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds); | ||||
|         this.osmSourceZoomLevel = state.osmApiTileSize.data; | ||||
|         const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12)) | ||||
|         const expiryInSeconds = Math.min( | ||||
|             ...(state.layoutToUse?.layers?.map((l) => l.maxAgeOfCache) ?? []) | ||||
|         ) | ||||
|         this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds) | ||||
|         this.osmSourceZoomLevel = state.osmApiTileSize.data | ||||
|         const useOsmApi = state.locationControl.map( | ||||
|             (l) => l.zoom > (state.overpassMaxZoom.data ?? 12) | ||||
|         ) | ||||
|         this.relationTracker = new RelationsTracker() | ||||
| 
 | ||||
|         state.changes.allChanges.addCallbackAndRun(allChanges => { | ||||
|             allChanges.filter(ch => ch.id < 0 && ch.changes !== undefined) | ||||
|                 .map(ch => ch.changes) | ||||
|                 .filter(coor => coor["lat"] !== undefined && coor["lon"] !== undefined) | ||||
|                 .forEach(coor => { | ||||
|                     state.layoutToUse.layers.forEach(l => self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"])) | ||||
|         state.changes.allChanges.addCallbackAndRun((allChanges) => { | ||||
|             allChanges | ||||
|                 .filter((ch) => ch.id < 0 && ch.changes !== undefined) | ||||
|                 .map((ch) => ch.changes) | ||||
|                 .filter((coor) => coor["lat"] !== undefined && coor["lon"] !== undefined) | ||||
|                 .forEach((coor) => { | ||||
|                     state.layoutToUse.layers.forEach((l) => | ||||
|                         self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"]) | ||||
|                     ) | ||||
|                 }) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         this.sufficientlyZoomed = state.locationControl.map(location => { | ||||
|         this.sufficientlyZoomed = state.locationControl.map((location) => { | ||||
|             if (location?.zoom === undefined) { | ||||
|                     return false; | ||||
|                 return false | ||||
|             } | ||||
|                 let minzoom = Math.min(...state.filteredLayers.data.map(layer => layer.layerDef.minzoom ?? 18)); | ||||
|                 return location.zoom >= minzoom; | ||||
|             } | ||||
|         ); | ||||
|             let minzoom = Math.min( | ||||
|                 ...state.filteredLayers.data.map((layer) => layer.layerDef.minzoom ?? 18) | ||||
|             ) | ||||
|             return location.zoom >= minzoom | ||||
|         }) | ||||
| 
 | ||||
|         const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed) | ||||
| 
 | ||||
|  | @ -111,9 +117,11 @@ export default class FeaturePipeline { | |||
|         this.perLayerHierarchy = perLayerHierarchy | ||||
| 
 | ||||
|         // Given a tile, wraps it and passes it on to render (handled by 'handleFeatureSource'
 | ||||
|         function patchedHandleFeatureSource(src: FeatureSourceForLayer & IndexedFeatureSource & Tiled) { | ||||
|         function patchedHandleFeatureSource( | ||||
|             src: FeatureSourceForLayer & IndexedFeatureSource & Tiled | ||||
|         ) { | ||||
|             // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
 | ||||
|             const withChanges = new ChangeGeometryApplicator(src, state.changes); | ||||
|             const withChanges = new ChangeGeometryApplicator(src, state.changes) | ||||
|             const srcFiltered = new FilteringFeatureSource(state, src.tileIndex, withChanges) | ||||
| 
 | ||||
|             handleFeatureSource(srcFiltered) | ||||
|  | @ -127,31 +135,29 @@ export default class FeaturePipeline { | |||
|         function handlePriviligedFeatureSource(src: FeatureSourceForLayer & Tiled) { | ||||
|             // Passthrough to passed function, except that it registers as well
 | ||||
|             handleFeatureSource(src) | ||||
|             src.features.addCallbackAndRunD(fs => { | ||||
|                 fs.forEach(ff => state.allElements.addOrGetElement(ff.feature)) | ||||
|             src.features.addCallbackAndRunD((fs) => { | ||||
|                 fs.forEach((ff) => state.allElements.addOrGetElement(ff.feature)) | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         for (const filteredLayer of state.filteredLayers.data) { | ||||
|             const id = filteredLayer.layerDef.id | ||||
|             const source = filteredLayer.layerDef.source | ||||
| 
 | ||||
|             const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) => patchedHandleFeatureSource(tile)) | ||||
|             const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) => | ||||
|                 patchedHandleFeatureSource(tile) | ||||
|             ) | ||||
|             perLayerHierarchy.set(id, hierarchy) | ||||
| 
 | ||||
|             this.freshnesses.set(id, new TileFreshnessCalculator()) | ||||
| 
 | ||||
|             if (id === "type_node") { | ||||
| 
 | ||||
|                 this.fullNodeDatabase = new FullNodeDatabaseSource( | ||||
|                     filteredLayer, | ||||
|                     tile => { | ||||
|                 this.fullNodeDatabase = new FullNodeDatabaseSource(filteredLayer, (tile) => { | ||||
|                     new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||
|                     perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) | ||||
|                         tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) | ||||
|                     }); | ||||
|                 continue; | ||||
|                     tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) | ||||
|                 }) | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (id === "gps_location") { | ||||
|  | @ -187,13 +193,15 @@ export default class FeaturePipeline { | |||
|                 // We load the cached values and register them
 | ||||
|                 // Getting data from upstream happens a bit lower
 | ||||
|                 localTileSaver.LoadTilesFromDisk( | ||||
|                     state.currentBounds, state.locationControl, | ||||
|                     (tileIndex, freshness) => self.freshnesses.get(id).addTileLoad(tileIndex, freshness), | ||||
|                     state.currentBounds, | ||||
|                     state.locationControl, | ||||
|                     (tileIndex, freshness) => | ||||
|                         self.freshnesses.get(id).addTileLoad(tileIndex, freshness), | ||||
|                     (tile) => { | ||||
|                         console.debug("Loaded tile ", id, tile.tileIndex, "from local cache") | ||||
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||
|                         hierarchy.registerTile(tile); | ||||
|                         tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) | ||||
|                         hierarchy.registerTile(tile) | ||||
|                         tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) | ||||
|                     } | ||||
|                 ) | ||||
| 
 | ||||
|  | @ -213,47 +221,48 @@ export default class FeaturePipeline { | |||
|                         registerTile: (tile) => { | ||||
|                             new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||
|                             perLayerHierarchy.get(id).registerTile(tile) | ||||
|                             tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) | ||||
|                         } | ||||
|                             tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) | ||||
|                         }, | ||||
|                     }) | ||||
|                 } else { | ||||
|                     new RegisteringAllFromFeatureSourceActor(src, state.allElements) | ||||
|                     perLayerHierarchy.get(id).registerTile(src) | ||||
|                     src.features.addCallbackAndRunD(_ => self.onNewDataLoaded(src)) | ||||
|                     src.features.addCallbackAndRunD((_) => self.onNewDataLoaded(src)) | ||||
|                 } | ||||
|             } else { | ||||
|                 new DynamicGeoJsonTileSource( | ||||
|                     filteredLayer, | ||||
|                     tile => { | ||||
|                     (tile) => { | ||||
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||
|                         perLayerHierarchy.get(id).registerTile(tile) | ||||
|                         tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) | ||||
|                         tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) | ||||
|                     }, | ||||
|                     state | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const osmFeatureSource = new OsmFeatureSource({ | ||||
|             isActive: useOsmApi, | ||||
|             neededTiles: neededTilesFromOsm, | ||||
|             handleTile: tile => { | ||||
|             handleTile: (tile) => { | ||||
|                 new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||
|                 if (tile.layer.layerDef.maxAgeOfCache > 0) { | ||||
|                     const saver = self.localStorageSavers.get(tile.layer.layerDef.id) | ||||
|                     if (saver === undefined) { | ||||
|                         console.error("No localStorageSaver found for layer ", tile.layer.layerDef.id) | ||||
|                         console.error( | ||||
|                             "No localStorageSaver found for layer ", | ||||
|                             tile.layer.layerDef.id | ||||
|                         ) | ||||
|                     } | ||||
|                     saver?.addTile(tile) | ||||
|                 } | ||||
|                 perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) | ||||
|                 tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) | ||||
| 
 | ||||
|                 tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) | ||||
|             }, | ||||
|             state: state, | ||||
|             markTileVisited: (tileId) => | ||||
|                 state.filteredLayers.data.forEach(flayer => { | ||||
|                 state.filteredLayers.data.forEach((flayer) => { | ||||
|                     const layer = flayer.layerDef | ||||
|                     if (layer.maxAgeOfCache > 0) { | ||||
|                         const saver = self.localStorageSavers.get(layer.id) | ||||
|  | @ -264,21 +273,24 @@ export default class FeaturePipeline { | |||
|                         } | ||||
|                     } | ||||
|                     self.freshnesses.get(layer.id).addTileLoad(tileId, new Date()) | ||||
|                 }) | ||||
|                 }), | ||||
|         }) | ||||
| 
 | ||||
|         if (this.fullNodeDatabase !== undefined) { | ||||
|             osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => this.fullNodeDatabase.handleOsmJson(osmJson, tileId)) | ||||
|             osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => | ||||
|                 this.fullNodeDatabase.handleOsmJson(osmJson, tileId) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const updater = this.initOverpassUpdater(state, useOsmApi) | ||||
|         this.overpassUpdater = updater; | ||||
|         this.overpassUpdater = updater | ||||
|         this.timeout = updater.timeout | ||||
| 
 | ||||
|         // Actually load data from the overpass source
 | ||||
|         new PerLayerFeatureSourceSplitter(state.filteredLayers, | ||||
|             (source) => TiledFeatureSource.createHierarchy(source, { | ||||
|         new PerLayerFeatureSourceSplitter( | ||||
|             state.filteredLayers, | ||||
|             (source) => | ||||
|                 TiledFeatureSource.createHierarchy(source, { | ||||
|                     layer: source.layer, | ||||
|                     minZoomLevel: source.layer.layerDef.minzoom, | ||||
|                     noDuplicates: true, | ||||
|  | @ -287,87 +299,102 @@ export default class FeaturePipeline { | |||
|                     registerTile: (tile) => { | ||||
|                         // We save the tile data for the given layer to local storage - data sourced from overpass
 | ||||
|                         self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile) | ||||
|                     perLayerHierarchy.get(source.layer.layerDef.id).registerTile(new RememberingSource(tile)) | ||||
|                     tile.features.addCallbackAndRunD(f => { | ||||
|                         perLayerHierarchy | ||||
|                             .get(source.layer.layerDef.id) | ||||
|                             .registerTile(new RememberingSource(tile)) | ||||
|                         tile.features.addCallbackAndRunD((f) => { | ||||
|                             if (f.length === 0) { | ||||
|                                 return | ||||
|                             } | ||||
|                             self.onNewDataLoaded(tile) | ||||
|                         }) | ||||
| 
 | ||||
|                 } | ||||
|                     }, | ||||
|                 }), | ||||
|             updater, | ||||
|             { | ||||
|                 handleLeftovers: (leftOvers) => { | ||||
|                     console.warn("Overpass returned a few non-matched features:", leftOvers) | ||||
|                 }, | ||||
|             } | ||||
|             }) | ||||
| 
 | ||||
|         ) | ||||
| 
 | ||||
|         // Also load points/lines that are newly added.
 | ||||
|         const newGeometry = new NewGeometryFromChangesFeatureSource(state.changes, state.allElements, state.osmConnection._oauth_config.url) | ||||
|         this.newGeometryHandler = newGeometry; | ||||
|         newGeometry.features.addCallbackAndRun(geometries => { | ||||
|         const newGeometry = new NewGeometryFromChangesFeatureSource( | ||||
|             state.changes, | ||||
|             state.allElements, | ||||
|             state.osmConnection._oauth_config.url | ||||
|         ) | ||||
|         this.newGeometryHandler = newGeometry | ||||
|         newGeometry.features.addCallbackAndRun((geometries) => { | ||||
|             console.debug("New geometries are:", geometries) | ||||
|         }) | ||||
| 
 | ||||
|         new RegisteringAllFromFeatureSourceActor(newGeometry, state.allElements) | ||||
|         // A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next
 | ||||
|         new PerLayerFeatureSourceSplitter(state.filteredLayers, | ||||
|         new PerLayerFeatureSourceSplitter( | ||||
|             state.filteredLayers, | ||||
|             (perLayer) => { | ||||
|                 // We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
 | ||||
|                 perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer) | ||||
|                 // AT last, we always apply the metatags whenever possible
 | ||||
|                 perLayer.features.addCallbackAndRunD(_ => { | ||||
|                     self.onNewDataLoaded(perLayer); | ||||
|                 perLayer.features.addCallbackAndRunD((_) => { | ||||
|                     self.onNewDataLoaded(perLayer) | ||||
|                 }) | ||||
| 
 | ||||
|             }, | ||||
|             newGeometry, | ||||
|             { | ||||
|                 handleLeftovers: (leftOvers) => { | ||||
|                     console.warn("Got some leftovers from the filteredLayers: ", leftOvers) | ||||
|                 } | ||||
|                 }, | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
|         this.runningQuery = updater.runningQuery.map( | ||||
|             overpass => { | ||||
|                 console.log("FeaturePipeline: runningQuery state changed: Overpass", overpass ? "is querying," : "is idle,", | ||||
|                     "osmFeatureSource is", osmFeatureSource.isRunning ? "is running and needs " + neededTilesFromOsm.data?.length + " tiles (already got " + osmFeatureSource.downloadedTiles.size + " tiles )" : "is idle") | ||||
|                 return overpass || osmFeatureSource.isRunning.data; | ||||
|             }, [osmFeatureSource.isRunning] | ||||
|             (overpass) => { | ||||
|                 console.log( | ||||
|                     "FeaturePipeline: runningQuery state changed: Overpass", | ||||
|                     overpass ? "is querying," : "is idle,", | ||||
|                     "osmFeatureSource is", | ||||
|                     osmFeatureSource.isRunning | ||||
|                         ? "is running and needs " + | ||||
|                               neededTilesFromOsm.data?.length + | ||||
|                               " tiles (already got " + | ||||
|                               osmFeatureSource.downloadedTiles.size + | ||||
|                               " tiles )" | ||||
|                         : "is idle" | ||||
|                 ) | ||||
|                 return overpass || osmFeatureSource.isRunning.data | ||||
|             }, | ||||
|             [osmFeatureSource.isRunning] | ||||
|         ) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public GetAllFeaturesWithin(bbox: BBox): OsmFeature[][] { | ||||
|         const self = this | ||||
|         const tiles: OsmFeature[][] = [] | ||||
|         Array.from(this.perLayerHierarchy.keys()) | ||||
|             .forEach(key => { | ||||
|                 const fetched : OsmFeature[][] = self.GetFeaturesWithin(key, bbox) | ||||
|                 tiles.push(...fetched); | ||||
|         Array.from(this.perLayerHierarchy.keys()).forEach((key) => { | ||||
|             const fetched: OsmFeature[][] = self.GetFeaturesWithin(key, bbox) | ||||
|             tiles.push(...fetched) | ||||
|         }) | ||||
|         return tiles; | ||||
|         return tiles | ||||
|     } | ||||
| 
 | ||||
|     public GetAllFeaturesAndMetaWithin(bbox: BBox, layerIdWhitelist?: Set<string>):  | ||||
|         {features: OsmFeature[], layer: string}[] { | ||||
|     public GetAllFeaturesAndMetaWithin( | ||||
|         bbox: BBox, | ||||
|         layerIdWhitelist?: Set<string> | ||||
|     ): { features: OsmFeature[]; layer: string }[] { | ||||
|         const self = this | ||||
|         const tiles :{features: any[], layer: string}[]= [] | ||||
|         Array.from(this.perLayerHierarchy.keys()) | ||||
|             .forEach(key => { | ||||
|                 if(layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)){ | ||||
|                     return; | ||||
|         const tiles: { features: any[]; layer: string }[] = [] | ||||
|         Array.from(this.perLayerHierarchy.keys()).forEach((key) => { | ||||
|             if (layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)) { | ||||
|                 return | ||||
|             } | ||||
|             return tiles.push({ | ||||
|                 layer: key, | ||||
|                     features: [].concat(...self.GetFeaturesWithin(key, bbox)) | ||||
|                 }); | ||||
|                 features: [].concat(...self.GetFeaturesWithin(key, bbox)), | ||||
|             }) | ||||
|         return tiles; | ||||
|         }) | ||||
|         return tiles | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -380,16 +407,24 @@ export default class FeaturePipeline { | |||
|         } | ||||
|         const requestedHierarchy = this.perLayerHierarchy.get(layerId) | ||||
|         if (requestedHierarchy === undefined) { | ||||
|             console.warn("Layer ", layerId, "is not defined. Try one of ", Array.from(this.perLayerHierarchy.keys())) | ||||
|             return undefined; | ||||
|             console.warn( | ||||
|                 "Layer ", | ||||
|                 layerId, | ||||
|                 "is not defined. Try one of ", | ||||
|                 Array.from(this.perLayerHierarchy.keys()) | ||||
|             ) | ||||
|             return undefined | ||||
|         } | ||||
|         return TileHierarchyTools.getTiles(requestedHierarchy, bbox) | ||||
|             .filter(featureSource => featureSource.features?.data !== undefined) | ||||
|             .map(featureSource => featureSource.features.data.map(fs => fs.feature)) | ||||
|             .filter((featureSource) => featureSource.features?.data !== undefined) | ||||
|             .map((featureSource) => featureSource.features.data.map((fs) => fs.feature)) | ||||
|     } | ||||
| 
 | ||||
|     public GetTilesPerLayerWithin(bbox: BBox, handleTile: (tile: FeatureSourceForLayer & Tiled) => void) { | ||||
|         Array.from(this.perLayerHierarchy.values()).forEach(hierarchy => { | ||||
|     public GetTilesPerLayerWithin( | ||||
|         bbox: BBox, | ||||
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void | ||||
|     ) { | ||||
|         Array.from(this.perLayerHierarchy.values()).forEach((hierarchy) => { | ||||
|             TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile) | ||||
|         }) | ||||
|     } | ||||
|  | @ -399,16 +434,16 @@ export default class FeaturePipeline { | |||
|     } | ||||
| 
 | ||||
|     private freshnessForVisibleLayers(z: number, x: number, y: number): Date { | ||||
|         let oldestDate = undefined; | ||||
|         let oldestDate = undefined | ||||
|         for (const flayer of this.state.filteredLayers.data) { | ||||
|             if (!flayer.isDisplayed.data && !flayer.layerDef.forceLoad) { | ||||
|                 continue | ||||
|             } | ||||
|             if (this.state.locationControl.data.zoom < flayer.layerDef.minzoom) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
|             if (flayer.layerDef.maxAgeOfCache === 0) { | ||||
|                 return undefined; | ||||
|                 return undefined | ||||
|             } | ||||
|             const freshnessCalc = this.freshnesses.get(flayer.layerDef.id) | ||||
|             if (freshnessCalc === undefined) { | ||||
|  | @ -432,12 +467,13 @@ export default class FeaturePipeline { | |||
|      * */ | ||||
|     private getNeededTilesFromOsm(isSufficientlyZoomed: Store<boolean>): Store<number[]> { | ||||
|         const self = this | ||||
|         return this.state.currentBounds.map(bbox => { | ||||
|         return this.state.currentBounds.map( | ||||
|             (bbox) => { | ||||
|                 if (bbox === undefined) { | ||||
|                     return [] | ||||
|                 } | ||||
|                 if (!isSufficientlyZoomed.data) { | ||||
|                 return []; | ||||
|                     return [] | ||||
|                 } | ||||
|                 const osmSourceZoomLevel = self.osmSourceZoomLevel | ||||
|                 const range = bbox.containingTileRange(osmSourceZoomLevel) | ||||
|  | @ -447,30 +483,42 @@ export default class FeaturePipeline { | |||
|                     return undefined | ||||
|                 } | ||||
|                 Tiles.MapRange(range, (x, y) => { | ||||
|                 const i = Tiles.tile_index(osmSourceZoomLevel, x, y); | ||||
|                     const i = Tiles.tile_index(osmSourceZoomLevel, x, y) | ||||
|                     const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y) | ||||
|                     if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) { | ||||
|                     console.debug("Skipping tile", osmSourceZoomLevel, x, y, "as a decently fresh one is available") | ||||
|                         console.debug( | ||||
|                             "Skipping tile", | ||||
|                             osmSourceZoomLevel, | ||||
|                             x, | ||||
|                             y, | ||||
|                             "as a decently fresh one is available" | ||||
|                         ) | ||||
|                         // The cached tiles contain decently fresh data
 | ||||
|                     return undefined; | ||||
|                         return undefined | ||||
|                     } | ||||
|                     tileIndexes.push(i) | ||||
|                 }) | ||||
|                 return tileIndexes | ||||
|         }, [isSufficientlyZoomed]) | ||||
|             }, | ||||
|             [isSufficientlyZoomed] | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private initOverpassUpdater(state: { | ||||
|         allElements: ElementStorage; | ||||
|         layoutToUse: LayoutConfig, | ||||
|         currentBounds: Store<BBox>, | ||||
|         locationControl: Store<Loc>, | ||||
|         readonly overpassUrl: Store<string[]>; | ||||
|         readonly overpassTimeout: Store<number>; | ||||
|         readonly overpassMaxZoom: Store<number>, | ||||
|     }, useOsmApi: Store<boolean>): OverpassFeatureSource { | ||||
|         const minzoom = Math.min(...state.layoutToUse.layers.map(layer => layer.minzoom)) | ||||
|         const overpassIsActive = state.currentBounds.map(bbox => { | ||||
|     private initOverpassUpdater( | ||||
|         state: { | ||||
|             allElements: ElementStorage | ||||
|             layoutToUse: LayoutConfig | ||||
|             currentBounds: Store<BBox> | ||||
|             locationControl: Store<Loc> | ||||
|             readonly overpassUrl: Store<string[]> | ||||
|             readonly overpassTimeout: Store<number> | ||||
|             readonly overpassMaxZoom: Store<number> | ||||
|         }, | ||||
|         useOsmApi: Store<boolean> | ||||
|     ): OverpassFeatureSource { | ||||
|         const minzoom = Math.min(...state.layoutToUse.layers.map((layer) => layer.minzoom)) | ||||
|         const overpassIsActive = state.currentBounds.map( | ||||
|             (bbox) => { | ||||
|                 if (bbox === undefined) { | ||||
|                     console.debug("Disabling overpass source: no bbox") | ||||
|                     return false | ||||
|  | @ -479,7 +527,7 @@ export default class FeaturePipeline { | |||
|                 if (zoom < minzoom) { | ||||
|                     // We are zoomed out over the zoomlevel of any layer
 | ||||
|                     console.debug("Disabling overpass source: zoom < minzoom") | ||||
|                 return false; | ||||
|                     return false | ||||
|                 } | ||||
| 
 | ||||
|                 const range = bbox.containingTileRange(zoom) | ||||
|  | @ -487,58 +535,64 @@ export default class FeaturePipeline { | |||
|                     // Let's assume we don't have so much data cached
 | ||||
|                     return true | ||||
|                 } | ||||
|             const self = this; | ||||
|             const allFreshnesses = Tiles.MapRange(range, (x, y) => self.freshnessForVisibleLayers(zoom, x, y)) | ||||
|             return allFreshnesses.some(freshness => freshness === undefined || freshness < this.oldestAllowedDate) | ||||
|         }, [state.locationControl]) | ||||
|                 const self = this | ||||
|                 const allFreshnesses = Tiles.MapRange(range, (x, y) => | ||||
|                     self.freshnessForVisibleLayers(zoom, x, y) | ||||
|                 ) | ||||
|                 return allFreshnesses.some( | ||||
|                     (freshness) => freshness === undefined || freshness < this.oldestAllowedDate | ||||
|                 ) | ||||
|             }, | ||||
|             [state.locationControl] | ||||
|         ) | ||||
| 
 | ||||
|         const self = this; | ||||
|         const updater = new OverpassFeatureSource(state, | ||||
|             { | ||||
|                 padToTiles: state.locationControl.map(l => Math.min(15, l.zoom + 1)), | ||||
|         const self = this | ||||
|         const updater = new OverpassFeatureSource(state, { | ||||
|             padToTiles: state.locationControl.map((l) => Math.min(15, l.zoom + 1)), | ||||
|             relationTracker: this.relationTracker, | ||||
|                 isActive: useOsmApi.map(b => !b && overpassIsActive.data, [overpassIsActive]), | ||||
|             isActive: useOsmApi.map((b) => !b && overpassIsActive.data, [overpassIsActive]), | ||||
|             freshnesses: this.freshnesses, | ||||
|             onBboxLoaded: (bbox, date, downloadedLayers, paddedToZoomLevel) => { | ||||
|                 Tiles.MapRange(bbox.containingTileRange(paddedToZoomLevel), (x, y) => { | ||||
|                     const tileIndex = Tiles.tile_index(paddedToZoomLevel, x, y) | ||||
|                         downloadedLayers.forEach(layer => { | ||||
|                     downloadedLayers.forEach((layer) => { | ||||
|                         self.freshnesses.get(layer.id).addTileLoad(tileIndex, date) | ||||
|                         self.localStorageSavers.get(layer.id)?.MarkVisited(tileIndex, date) | ||||
|                     }) | ||||
|                 }) | ||||
| 
 | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             }, | ||||
|         }) | ||||
| 
 | ||||
|         // Register everything in the state' 'AllElements'
 | ||||
|         new RegisteringAllFromFeatureSourceActor(updater, state.allElements) | ||||
|         return updater; | ||||
|         return updater | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Builds upon 'GetAllFeaturesAndMetaWithin', but does stricter BBOX-checking and applies the filters | ||||
|      */ | ||||
|     public getAllVisibleElementsWithmeta(bbox: BBox): { center: [number, number], element: OsmFeature, layer: LayerConfig }[] { | ||||
|     public getAllVisibleElementsWithmeta( | ||||
|         bbox: BBox | ||||
|     ): { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] { | ||||
|         if (bbox === undefined) { | ||||
|             console.warn("No bbox") | ||||
|             return [] | ||||
|         } | ||||
| 
 | ||||
|         const layers = Utils.toIdRecord(this.state.layoutToUse.layers) | ||||
|         const elementsWithMeta: { features: OsmFeature[], layer: string }[] = this.GetAllFeaturesAndMetaWithin(bbox) | ||||
|         const elementsWithMeta: { features: OsmFeature[]; layer: string }[] = | ||||
|             this.GetAllFeaturesAndMetaWithin(bbox) | ||||
| 
 | ||||
|         let elements: {center: [number, number], element: OsmFeature, layer: LayerConfig }[] = [] | ||||
|         let elements: { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] = [] | ||||
|         let seenElements = new Set<string>() | ||||
|         for (const elementsWithMetaElement of elementsWithMeta) { | ||||
|             const layer = layers[elementsWithMetaElement.layer] | ||||
|             if(layer.title === undefined){ | ||||
|             if (layer.title === undefined) { | ||||
|                 continue | ||||
|             } | ||||
|             const filtered = this.state.filteredLayers.data.find(fl => fl.layerDef == layer); | ||||
|             const filtered = this.state.filteredLayers.data.find((fl) => fl.layerDef == layer) | ||||
|             for (let i = 0; i < elementsWithMetaElement.features.length; i++) { | ||||
|                 const element = elementsWithMetaElement.features[i]; | ||||
|                 const element = elementsWithMetaElement.features[i] | ||||
|                 if (!filtered.isDisplayed.data) { | ||||
|                     continue | ||||
|                 } | ||||
|  | @ -552,35 +606,38 @@ export default class FeaturePipeline { | |||
|                 if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) { | ||||
|                     continue | ||||
|                 } | ||||
|                 const activeFilters: FilterState[] = Array.from(filtered.appliedFilters.data.values()); | ||||
|                 if (!activeFilters.every(filter => filter?.currentFilter === undefined || filter?.currentFilter?.matchesProperties(element.properties))) { | ||||
|                 const activeFilters: FilterState[] = Array.from( | ||||
|                     filtered.appliedFilters.data.values() | ||||
|                 ) | ||||
|                 if ( | ||||
|                     !activeFilters.every( | ||||
|                         (filter) => | ||||
|                             filter?.currentFilter === undefined || | ||||
|                             filter?.currentFilter?.matchesProperties(element.properties) | ||||
|                     ) | ||||
|                 ) { | ||||
|                     continue | ||||
|                 } | ||||
|                 const center = GeoOperations.centerpointCoordinates(element); | ||||
|                 const center = GeoOperations.centerpointCoordinates(element) | ||||
|                 elements.push({ | ||||
|                     element, | ||||
|                     center, | ||||
|                     layer: layers[elementsWithMetaElement.layer], | ||||
|                 }) | ||||
| 
 | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|       | ||||
| 
 | ||||
|         return elements; | ||||
|         return elements | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Inject a new point | ||||
|      */ | ||||
|     InjectNewPoint(geojson) { | ||||
|         this.newGeometryHandler.features.data.push({ | ||||
|             feature: geojson, | ||||
|             freshness: new Date() | ||||
|             freshness: new Date(), | ||||
|         }) | ||||
|         this.newGeometryHandler.features.ping(); | ||||
|         this.newGeometryHandler.features.ping() | ||||
|     } | ||||
| } | ||||
|  | @ -1,19 +1,19 @@ | |||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import {BBox} from "../BBox"; | ||||
| import {Feature, Geometry} from "@turf/turf"; | ||||
| import {OsmFeature} from "../../Models/OsmFeature"; | ||||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import { BBox } from "../BBox" | ||||
| import { Feature, Geometry } from "@turf/turf" | ||||
| import { OsmFeature } from "../../Models/OsmFeature" | ||||
| 
 | ||||
| export default interface FeatureSource { | ||||
|     features: Store<{ feature: OsmFeature, freshness: Date }[]>; | ||||
|     features: Store<{ feature: OsmFeature; freshness: Date }[]> | ||||
|     /** | ||||
|      * Mainly used for debuging | ||||
|      */ | ||||
|     name: string; | ||||
|     name: string | ||||
| } | ||||
| 
 | ||||
| export interface Tiled { | ||||
|     tileIndex: number, | ||||
|     tileIndex: number | ||||
|     bbox: BBox | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,8 +1,7 @@ | |||
| import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource"; | ||||
| import {Store} from "../UIEventSource"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import SimpleFeatureSource from "./Sources/SimpleFeatureSource"; | ||||
| 
 | ||||
| import FeatureSource, { FeatureSourceForLayer, Tiled } from "./FeatureSource" | ||||
| import { Store } from "../UIEventSource" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import SimpleFeatureSource from "./Sources/SimpleFeatureSource" | ||||
| 
 | ||||
| /** | ||||
|  * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) | ||||
|  | @ -10,30 +9,30 @@ import SimpleFeatureSource from "./Sources/SimpleFeatureSource"; | |||
|  * In any case, this featureSource marks the objects with _matching_layer_id | ||||
|  */ | ||||
| export default class PerLayerFeatureSourceSplitter { | ||||
| 
 | ||||
|     constructor(layers: Store<FilteredLayer[]>, | ||||
|     constructor( | ||||
|         layers: Store<FilteredLayer[]>, | ||||
|         handleLayerData: (source: FeatureSourceForLayer & Tiled) => void, | ||||
|         upstream: FeatureSource, | ||||
|         options?: { | ||||
|                     tileIndex?: number, | ||||
|             tileIndex?: number | ||||
|             handleLeftovers?: (featuresWithoutLayer: any[]) => void | ||||
|                 }) { | ||||
| 
 | ||||
|         } | ||||
|     ) { | ||||
|         const knownLayers = new Map<string, SimpleFeatureSource>() | ||||
| 
 | ||||
|         function update() { | ||||
|             const features = upstream.features?.data; | ||||
|             const features = upstream.features?.data | ||||
|             if (features === undefined) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             if (layers.data === undefined || layers.data.length === 0) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             // We try to figure out (for each feature) in which feature store it should be saved.
 | ||||
|             // Note that this splitter is only run when it is invoked by the overpass feature source, so we can't be sure in which layer it should go
 | ||||
| 
 | ||||
|             const featuresPerLayer = new Map<string, { feature, freshness } []>(); | ||||
|             const featuresPerLayer = new Map<string, { feature; freshness }[]>() | ||||
|             const noLayerFound = [] | ||||
| 
 | ||||
|             for (const layer of layers.data) { | ||||
|  | @ -41,19 +40,19 @@ export default class PerLayerFeatureSourceSplitter { | |||
|             } | ||||
| 
 | ||||
|             for (const f of features) { | ||||
|                 let foundALayer = false; | ||||
|                 let foundALayer = false | ||||
|                 for (const layer of layers.data) { | ||||
|                     if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) { | ||||
|                         // We have found our matching layer!
 | ||||
|                         featuresPerLayer.get(layer.layerDef.id).push(f) | ||||
|                         foundALayer = true; | ||||
|                         foundALayer = true | ||||
|                         if (!layer.layerDef.passAllFeatures) { | ||||
|                             // If not 'passAllFeatures', we are done for this feature
 | ||||
|                             break | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 if(!foundALayer){ | ||||
|                 if (!foundALayer) { | ||||
|                     noLayerFound.push(f) | ||||
|                 } | ||||
|             } | ||||
|  | @ -61,11 +60,11 @@ export default class PerLayerFeatureSourceSplitter { | |||
|             // At this point, we have our features per layer as a list
 | ||||
|             // We assign them to the correct featureSources
 | ||||
|             for (const layer of layers.data) { | ||||
|                 const id = layer.layerDef.id; | ||||
|                 const id = layer.layerDef.id | ||||
|                 const features = featuresPerLayer.get(id) | ||||
|                 if (features === undefined) { | ||||
|                     // No such features for this layer
 | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 let featureSource = knownLayers.get(id) | ||||
|  | @ -86,7 +85,7 @@ export default class PerLayerFeatureSourceSplitter { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         layers.addCallback(_ => update()) | ||||
|         upstream.features.addCallbackAndRunD(_ => update()) | ||||
|         layers.addCallback((_) => update()) | ||||
|         upstream.features.addCallbackAndRunD((_) => update()) | ||||
|     } | ||||
| } | ||||
|  | @ -1,52 +1,52 @@ | |||
| /** | ||||
|  * Applies geometry changes from 'Changes' onto every feature of a featureSource | ||||
|  */ | ||||
| import {Changes} from "../../Osm/Changes"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import {FeatureSourceForLayer, IndexedFeatureSource} from "../FeatureSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {ChangeDescription, ChangeDescriptionTools} from "../../Osm/Actions/ChangeDescription"; | ||||
| 
 | ||||
| import { Changes } from "../../Osm/Changes" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import { FeatureSourceForLayer, IndexedFeatureSource } from "../FeatureSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription" | ||||
| 
 | ||||
| export default class ChangeGeometryApplicator implements FeatureSourceForLayer { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|     public readonly name: string; | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = | ||||
|         new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||
|     public readonly name: string | ||||
|     public readonly layer: FilteredLayer | ||||
|     private readonly source: IndexedFeatureSource; | ||||
|     private readonly changes: Changes; | ||||
|     private readonly source: IndexedFeatureSource | ||||
|     private readonly changes: Changes | ||||
| 
 | ||||
|     constructor(source: (IndexedFeatureSource & FeatureSourceForLayer), changes: Changes) { | ||||
|         this.source = source; | ||||
|         this.changes = changes; | ||||
|     constructor(source: IndexedFeatureSource & FeatureSourceForLayer, changes: Changes) { | ||||
|         this.source = source | ||||
|         this.changes = changes | ||||
|         this.layer = source.layer | ||||
| 
 | ||||
|         this.name = "ChangesApplied(" + source.name + ")" | ||||
|         this.features = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined) | ||||
| 
 | ||||
|         const self = this; | ||||
|         source.features.addCallbackAndRunD(_ => self.update()) | ||||
| 
 | ||||
|         changes.allChanges.addCallbackAndRunD(_ => self.update()) | ||||
|         const self = this | ||||
|         source.features.addCallbackAndRunD((_) => self.update()) | ||||
| 
 | ||||
|         changes.allChanges.addCallbackAndRunD((_) => self.update()) | ||||
|     } | ||||
| 
 | ||||
|     private update() { | ||||
|         const upstreamFeatures = this.source.features.data | ||||
|         const upstreamIds = this.source.containedIds.data | ||||
|         const changesToApply = this.changes.allChanges.data | ||||
|             ?.filter(ch => | ||||
|         const changesToApply = this.changes.allChanges.data?.filter( | ||||
|             (ch) => | ||||
|                 // Does upsteram have this element? If not, we skip
 | ||||
|                 upstreamIds.has(ch.type + "/" + ch.id) && | ||||
|                 // Are any (geometry) changes defined?
 | ||||
|                 ch.changes !== undefined && | ||||
|                 // Ignore new elements, they are handled by the NewGeometryFromChangesFeatureSource
 | ||||
|                 ch.id > 0) | ||||
|                 ch.id > 0 | ||||
|         ) | ||||
| 
 | ||||
|         if (changesToApply === undefined || changesToApply.length === 0) { | ||||
|             // No changes to apply!
 | ||||
|             // Pass the original feature and lets continue our day
 | ||||
|             this.features.setData(upstreamFeatures); | ||||
|             return; | ||||
|             this.features.setData(upstreamFeatures) | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         const changesPerId = new Map<string, ChangeDescription[]>() | ||||
|  | @ -58,27 +58,32 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer { | |||
|                 changesPerId.set(key, [ch]) | ||||
|             } | ||||
|         } | ||||
|         const newFeatures: { feature: any, freshness: Date }[] = [] | ||||
|         const newFeatures: { feature: any; freshness: Date }[] = [] | ||||
|         for (const feature of upstreamFeatures) { | ||||
|             const changesForFeature = changesPerId.get(feature.feature.properties.id) | ||||
|             if (changesForFeature === undefined) { | ||||
|                 // No changes for this element
 | ||||
|                 newFeatures.push(feature) | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             // Allright! We have a feature to rewrite!
 | ||||
|             const copy = { | ||||
|                 ...feature | ||||
|                 ...feature, | ||||
|             } | ||||
|             // We only apply the last change as that one'll have the latest geometry
 | ||||
|             const change = changesForFeature[changesForFeature.length - 1] | ||||
|             copy.feature.geometry = ChangeDescriptionTools.getGeojsonGeometry(change) | ||||
|             console.log("Applying a geometry change onto:", feature,"The change is:", change,"which becomes:", copy) | ||||
|             console.log( | ||||
|                 "Applying a geometry change onto:", | ||||
|                 feature, | ||||
|                 "The change is:", | ||||
|                 change, | ||||
|                 "which becomes:", | ||||
|                 copy | ||||
|             ) | ||||
|             newFeatures.push(copy) | ||||
|         } | ||||
|         this.features.setData(newFeatures) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,99 +1,112 @@ | |||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { Tiles } from "../../../Models/TileRange" | ||||
| import { BBox } from "../../BBox" | ||||
| 
 | ||||
| 
 | ||||
| export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource { | ||||
| 
 | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|     public readonly name; | ||||
| export default class FeatureSourceMerger | ||||
|     implements FeatureSourceForLayer, Tiled, IndexedFeatureSource | ||||
| { | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource< | ||||
|         { feature: any; freshness: Date }[] | ||||
|     >([]) | ||||
|     public readonly name | ||||
|     public readonly layer: FilteredLayer | ||||
|     public readonly tileIndex: number; | ||||
|     public readonly bbox: BBox; | ||||
|     public readonly containedIds: UIEventSource<Set<string>> = new UIEventSource<Set<string>>(new Set()) | ||||
|     private readonly _sources: UIEventSource<FeatureSource[]>; | ||||
|     public readonly tileIndex: number | ||||
|     public readonly bbox: BBox | ||||
|     public readonly containedIds: UIEventSource<Set<string>> = new UIEventSource<Set<string>>( | ||||
|         new Set() | ||||
|     ) | ||||
|     private readonly _sources: UIEventSource<FeatureSource[]> | ||||
|     /** | ||||
|      * Merges features from different featureSources for a single layer | ||||
|      * Uses the freshest feature available in the case multiple sources offer data with the same identifier | ||||
|      */ | ||||
|     constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource<FeatureSource[]>) { | ||||
|         this.tileIndex = tileIndex; | ||||
|         this.bbox = bbox; | ||||
|         this._sources = sources; | ||||
|         this.layer = layer; | ||||
|         this.name = "FeatureSourceMerger(" + layer.layerDef.id + ", " + Tiles.tile_from_index(tileIndex).join(",") + ")" | ||||
|         const self = this; | ||||
|     constructor( | ||||
|         layer: FilteredLayer, | ||||
|         tileIndex: number, | ||||
|         bbox: BBox, | ||||
|         sources: UIEventSource<FeatureSource[]> | ||||
|     ) { | ||||
|         this.tileIndex = tileIndex | ||||
|         this.bbox = bbox | ||||
|         this._sources = sources | ||||
|         this.layer = layer | ||||
|         this.name = | ||||
|             "FeatureSourceMerger(" + | ||||
|             layer.layerDef.id + | ||||
|             ", " + | ||||
|             Tiles.tile_from_index(tileIndex).join(",") + | ||||
|             ")" | ||||
|         const self = this | ||||
| 
 | ||||
|         const handledSources = new Set<FeatureSource>(); | ||||
|         const handledSources = new Set<FeatureSource>() | ||||
| 
 | ||||
|         sources.addCallbackAndRunD(sources => { | ||||
|             let newSourceRegistered = false; | ||||
|         sources.addCallbackAndRunD((sources) => { | ||||
|             let newSourceRegistered = false | ||||
|             for (let i = 0; i < sources.length; i++) { | ||||
|                 let source = sources[i]; | ||||
|                 let source = sources[i] | ||||
|                 if (handledSources.has(source)) { | ||||
|                     continue | ||||
|                 } | ||||
|                 handledSources.add(source) | ||||
|                 newSourceRegistered = true | ||||
|                 source.features.addCallback(() => { | ||||
|                     self.Update(); | ||||
|                 }); | ||||
|                     self.Update() | ||||
|                 }) | ||||
|                 if (newSourceRegistered) { | ||||
|                     self.Update(); | ||||
|                     self.Update() | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private Update() { | ||||
| 
 | ||||
|         let somethingChanged = false; | ||||
|         const all: Map<string, { feature: any, freshness: Date }> = new Map<string, { feature: any; freshness: Date }>(); | ||||
|         let somethingChanged = false | ||||
|         const all: Map<string, { feature: any; freshness: Date }> = new Map< | ||||
|             string, | ||||
|             { feature: any; freshness: Date } | ||||
|         >() | ||||
|         // We seed the dictionary with the previously loaded features
 | ||||
|         const oldValues = this.features.data ?? []; | ||||
|         const oldValues = this.features.data ?? [] | ||||
|         for (const oldValue of oldValues) { | ||||
|             all.set(oldValue.feature.id, oldValue) | ||||
|         } | ||||
| 
 | ||||
|         for (const source of this._sources.data) { | ||||
|             if (source?.features?.data === undefined) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
|             for (const f of source.features.data) { | ||||
|                 const id = f.feature.properties.id; | ||||
|                 const id = f.feature.properties.id | ||||
|                 if (!all.has(id)) { | ||||
|                     // This is a new feature
 | ||||
|                     somethingChanged = true; | ||||
|                     all.set(id, f); | ||||
|                     continue; | ||||
|                     somethingChanged = true | ||||
|                     all.set(id, f) | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 // This value has been seen already, either in a previous run or by a previous datasource
 | ||||
|                 // Let's figure out if something changed
 | ||||
|                 const oldV = all.get(id); | ||||
|                 const oldV = all.get(id) | ||||
|                 if (oldV.freshness < f.freshness) { | ||||
|                     // Jup, this feature is fresher
 | ||||
|                     all.set(id, f); | ||||
|                     somethingChanged = true; | ||||
|                     all.set(id, f) | ||||
|                     somethingChanged = true | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (!somethingChanged) { | ||||
|             // We don't bother triggering an update
 | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         const newList = []; | ||||
|         const newList = [] | ||||
|         all.forEach((value, _) => { | ||||
|             newList.push(value) | ||||
|         }) | ||||
|         this.containedIds.setData(new Set(all.keys())) | ||||
|         this.features.setData(newList); | ||||
|         this.features.setData(newList) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,34 +1,35 @@ | |||
| import {Store, UIEventSource} from "../../UIEventSource"; | ||||
| import FilteredLayer, {FilterState} from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import {ElementStorage} from "../../ElementStorage"; | ||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | ||||
| import {OsmFeature} from "../../../Models/OsmFeature"; | ||||
| import { Store, UIEventSource } from "../../UIEventSource" | ||||
| import FilteredLayer, { FilterState } from "../../../Models/FilteredLayer" | ||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import { BBox } from "../../BBox" | ||||
| import { ElementStorage } from "../../ElementStorage" | ||||
| import { TagsFilter } from "../../Tags/TagsFilter" | ||||
| import { OsmFeature } from "../../../Models/OsmFeature" | ||||
| 
 | ||||
| export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled { | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = | ||||
|         new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|     public readonly name; | ||||
|     public readonly layer: FilteredLayer; | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource< | ||||
|         { feature: any; freshness: Date }[] | ||||
|     >([]) | ||||
|     public readonly name | ||||
|     public readonly layer: FilteredLayer | ||||
|     public readonly tileIndex: number | ||||
|     public readonly bbox: BBox | ||||
|     private readonly upstream: FeatureSourceForLayer; | ||||
|     private readonly upstream: FeatureSourceForLayer | ||||
|     private readonly state: { | ||||
|         locationControl: Store<{ zoom: number }>;  | ||||
|         selectedElement: Store<any>, | ||||
|         globalFilters: Store<{ filter: FilterState }[]>, | ||||
|         locationControl: Store<{ zoom: number }> | ||||
|         selectedElement: Store<any> | ||||
|         globalFilters: Store<{ filter: FilterState }[]> | ||||
|         allElements: ElementStorage | ||||
|     }; | ||||
|     private readonly _alreadyRegistered = new Set<UIEventSource<any>>(); | ||||
|     } | ||||
|     private readonly _alreadyRegistered = new Set<UIEventSource<any>>() | ||||
|     private readonly _is_dirty = new UIEventSource(false) | ||||
|     private previousFeatureSet: Set<any> = undefined; | ||||
|     private previousFeatureSet: Set<any> = undefined | ||||
| 
 | ||||
|     constructor( | ||||
|         state: { | ||||
|             locationControl: Store<{ zoom: number }>, | ||||
|             selectedElement: Store<any>, | ||||
|             allElements: ElementStorage, | ||||
|             locationControl: Store<{ zoom: number }> | ||||
|             selectedElement: Store<any> | ||||
|             allElements: ElementStorage | ||||
|             globalFilters: Store<{ filter: FilterState }[]> | ||||
|         }, | ||||
|         tileIndex, | ||||
|  | @ -41,92 +42,95 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti | |||
|         this.upstream = upstream | ||||
|         this.state = state | ||||
| 
 | ||||
|         this.layer = upstream.layer; | ||||
|         const layer = upstream.layer; | ||||
|         const self = this; | ||||
|         this.layer = upstream.layer | ||||
|         const layer = upstream.layer | ||||
|         const self = this | ||||
|         upstream.features.addCallback(() => { | ||||
|             self.update(); | ||||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|         layer.appliedFilters.addCallback(_ => { | ||||
|             self.update() | ||||
|         }) | ||||
| 
 | ||||
|         this._is_dirty.stabilized(1000).addCallbackAndRunD(dirty => { | ||||
|         layer.appliedFilters.addCallback((_) => { | ||||
|             self.update() | ||||
|         }) | ||||
| 
 | ||||
|         this._is_dirty.stabilized(1000).addCallbackAndRunD((dirty) => { | ||||
|             if (dirty) { | ||||
|                 self.update() | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         metataggingUpdated?.addCallback(_ => { | ||||
|         metataggingUpdated?.addCallback((_) => { | ||||
|             self._is_dirty.setData(true) | ||||
|         }) | ||||
| 
 | ||||
|         state.globalFilters.addCallback(_ => { | ||||
|         state.globalFilters.addCallback((_) => { | ||||
|             self.update() | ||||
|         }) | ||||
| 
 | ||||
|         this.update(); | ||||
|         this.update() | ||||
|     } | ||||
| 
 | ||||
|     private update() { | ||||
|         const self = this; | ||||
|         const layer = this.upstream.layer; | ||||
|         const features: { feature: OsmFeature; freshness: Date }[] = (this.upstream.features.data ?? []); | ||||
|         const includedFeatureIds = new Set<string>(); | ||||
|         const globalFilters = self.state.globalFilters.data.map(f => f.filter); | ||||
|         const self = this | ||||
|         const layer = this.upstream.layer | ||||
|         const features: { feature: OsmFeature; freshness: Date }[] = | ||||
|             this.upstream.features.data ?? [] | ||||
|         const includedFeatureIds = new Set<string>() | ||||
|         const globalFilters = self.state.globalFilters.data.map((f) => f.filter) | ||||
|         const newFeatures = (features ?? []).filter((f) => { | ||||
| 
 | ||||
|             self.registerCallback(f.feature) | ||||
| 
 | ||||
|             const isShown: TagsFilter = layer.layerDef.isShown; | ||||
|             const tags = f.feature.properties; | ||||
|             if (isShown !== undefined && !isShown.matchesProperties(tags) ) { | ||||
|                 return false; | ||||
|             const isShown: TagsFilter = layer.layerDef.isShown | ||||
|             const tags = f.feature.properties | ||||
|             if (isShown !== undefined && !isShown.matchesProperties(tags)) { | ||||
|                 return false | ||||
|             } | ||||
| 
 | ||||
|             const tagsFilter = Array.from(layer.appliedFilters?.data?.values() ?? []) | ||||
|             for (const filter of tagsFilter) { | ||||
|                 const neededTags: TagsFilter = filter?.currentFilter | ||||
|                 if (neededTags !== undefined && !neededTags.matchesProperties(f.feature.properties)) { | ||||
|                 if ( | ||||
|                     neededTags !== undefined && | ||||
|                     !neededTags.matchesProperties(f.feature.properties) | ||||
|                 ) { | ||||
|                     // Hidden by the filter on the layer itself - we want to hide it no matter what
 | ||||
|                     return false; | ||||
|                     return false | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             for (const filter of globalFilters) { | ||||
|                 const neededTags: TagsFilter = filter?.currentFilter | ||||
|                 if (neededTags !== undefined && !neededTags.matchesProperties(f.feature.properties)) { | ||||
|                 if ( | ||||
|                     neededTags !== undefined && | ||||
|                     !neededTags.matchesProperties(f.feature.properties) | ||||
|                 ) { | ||||
|                     // Hidden by the filter on the layer itself - we want to hide it no matter what
 | ||||
|                     return false; | ||||
|                     return false | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             includedFeatureIds.add(f.feature.properties.id) | ||||
|             return true; | ||||
|         }); | ||||
|             return true | ||||
|         }) | ||||
| 
 | ||||
|         const previousSet = this.previousFeatureSet; | ||||
|         const previousSet = this.previousFeatureSet | ||||
|         this._is_dirty.setData(false) | ||||
| 
 | ||||
|         // Is there any difference between the two sets?
 | ||||
|         if (previousSet !== undefined && previousSet.size === includedFeatureIds.size) { | ||||
|             // The size of the sets is the same - they _might_ be identical
 | ||||
|             const newItemFound = Array.from(includedFeatureIds).some(id => !previousSet.has(id)) | ||||
|             const newItemFound = Array.from(includedFeatureIds).some((id) => !previousSet.has(id)) | ||||
|             if (!newItemFound) { | ||||
|                 // We know that:
 | ||||
|                 // - The sets have the same size
 | ||||
|                 // - Every item from the new set has been found in the old set
 | ||||
|                 // which means they are identical!
 | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         // Something new has been found!
 | ||||
|         this.features.setData(newFeatures); | ||||
| 
 | ||||
|         this.features.setData(newFeatures) | ||||
|     } | ||||
| 
 | ||||
|     private registerCallback(feature: any) { | ||||
|  | @ -139,11 +143,10 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti | |||
|         } | ||||
|         this._alreadyRegistered.add(src) | ||||
| 
 | ||||
|         const self = this; | ||||
|         const self = this | ||||
|         // Add a callback as a changed tag migh change the filter
 | ||||
|         src.addCallbackAndRunD(_ => { | ||||
|         src.addCallbackAndRunD((_) => { | ||||
|             self._is_dirty.setData(true) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,126 +1,122 @@ | |||
| /** | ||||
|  * Fetches a geojson file somewhere and passes it along | ||||
|  */ | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import {GeoOperations} from "../../GeoOperations"; | ||||
| 
 | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { Utils } from "../../../Utils" | ||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import { Tiles } from "../../../Models/TileRange" | ||||
| import { BBox } from "../../BBox" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| 
 | ||||
| export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | ||||
| 
 | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly state = new UIEventSource<undefined | {error: string} | "loaded">(undefined) | ||||
|     public readonly name; | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> | ||||
|     public readonly state = new UIEventSource<undefined | { error: string } | "loaded">(undefined) | ||||
|     public readonly name | ||||
|     public readonly isOsmCache: boolean | ||||
|     public readonly layer: FilteredLayer; | ||||
|     public readonly layer: FilteredLayer | ||||
|     public readonly tileIndex | ||||
|     public readonly bbox; | ||||
|     private readonly seenids: Set<string>; | ||||
|     private readonly idKey ?: string; | ||||
|     public readonly bbox | ||||
|     private readonly seenids: Set<string> | ||||
|     private readonly idKey?: string | ||||
| 
 | ||||
|     public constructor(flayer: FilteredLayer, | ||||
|     public constructor( | ||||
|         flayer: FilteredLayer, | ||||
|         zxy?: [number, number, number] | BBox, | ||||
|         options?: { | ||||
|             featureIdBlacklist?: Set<string> | ||||
|                        }) { | ||||
| 
 | ||||
|         } | ||||
|     ) { | ||||
|         if (flayer.layerDef.source.geojsonZoomLevel !== undefined && zxy === undefined) { | ||||
|             throw "Dynamic layers are not supported. Use 'DynamicGeoJsonTileSource instead" | ||||
|         } | ||||
| 
 | ||||
|         this.layer = flayer; | ||||
|         this.layer = flayer | ||||
|         this.idKey = flayer.layerDef.source.idKey | ||||
|         this.seenids = options?.featureIdBlacklist ?? new Set<string>() | ||||
|         let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); | ||||
|         let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id) | ||||
|         if (zxy !== undefined) { | ||||
|             let tile_bbox: BBox; | ||||
|             let tile_bbox: BBox | ||||
|             if (zxy instanceof BBox) { | ||||
|                 tile_bbox = zxy; | ||||
|                 tile_bbox = zxy | ||||
|             } else { | ||||
|                 const [z, x, y] = zxy; | ||||
|                 tile_bbox = BBox.fromTile(z, x, y); | ||||
|                 const [z, x, y] = zxy | ||||
|                 tile_bbox = BBox.fromTile(z, x, y) | ||||
| 
 | ||||
|                 this.tileIndex = Tiles.tile_index(z, x, y) | ||||
|                 this.bbox = BBox.fromTile(z, x, y) | ||||
|                 url = url | ||||
|                     .replace('{z}', "" + z) | ||||
|                     .replace('{x}', "" + x) | ||||
|                     .replace('{y}', "" + y) | ||||
|                     .replace("{z}", "" + z) | ||||
|                     .replace("{x}", "" + x) | ||||
|                     .replace("{y}", "" + y) | ||||
|             } | ||||
|             let bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox | ||||
|             let bounds: { minLat: number; maxLat: number; minLon: number; maxLon: number } = | ||||
|                 tile_bbox | ||||
|             if (this.layer.layerDef.source.mercatorCrs) { | ||||
|                 bounds = tile_bbox.toMercator() | ||||
|             } | ||||
| 
 | ||||
|             url = url | ||||
|                 .replace('{y_min}', "" + bounds.minLat) | ||||
|                 .replace('{y_max}', "" + bounds.maxLat) | ||||
|                 .replace('{x_min}', "" + bounds.minLon) | ||||
|                 .replace('{x_max}', "" + bounds.maxLon) | ||||
| 
 | ||||
| 
 | ||||
|                 .replace("{y_min}", "" + bounds.minLat) | ||||
|                 .replace("{y_max}", "" + bounds.maxLat) | ||||
|                 .replace("{x_min}", "" + bounds.minLon) | ||||
|                 .replace("{x_max}", "" + bounds.maxLon) | ||||
|         } else { | ||||
|             this.tileIndex = Tiles.tile_index(0, 0, 0) | ||||
|             this.bbox = BBox.global; | ||||
|             this.bbox = BBox.global | ||||
|         } | ||||
| 
 | ||||
|         this.name = "GeoJsonSource of " + url; | ||||
|         this.name = "GeoJsonSource of " + url | ||||
| 
 | ||||
|         this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer; | ||||
|         this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer | ||||
|         this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||
|         this.LoadJSONFrom(url) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private LoadJSONFrom(url: string) { | ||||
|         const eventSource = this.features; | ||||
|         const self = this; | ||||
|         const eventSource = this.features | ||||
|         const self = this | ||||
|         Utils.downloadJsonCached(url, 60 * 60) | ||||
|             .then(json => { | ||||
|             .then((json) => { | ||||
|                 self.state.setData("loaded") | ||||
|                 // TODO: move somewhere else, just for testing
 | ||||
|                 // Check for maproulette data
 | ||||
|                 if (url.startsWith("https://maproulette.org/api/v2/tasks/box/")) { | ||||
|                     console.log("MapRoulette data detected") | ||||
|                     const data = json; | ||||
|                     let maprouletteFeatures: any[] = []; | ||||
|                     data.forEach(element => { | ||||
|                     const data = json | ||||
|                     let maprouletteFeatures: any[] = [] | ||||
|                     data.forEach((element) => { | ||||
|                         maprouletteFeatures.push({ | ||||
|                             type: "Feature", | ||||
|                             geometry: { | ||||
|                                 type: "Point", | ||||
|                                 coordinates: [element.point.lng, element.point.lat] | ||||
|                                 coordinates: [element.point.lng, element.point.lat], | ||||
|                             }, | ||||
|                             properties: { | ||||
|                                 // Map all properties to the feature
 | ||||
|                                 ...element, | ||||
|                             } | ||||
|                         }); | ||||
|                     }); | ||||
|                     json.features = maprouletteFeatures; | ||||
|                             }, | ||||
|                         }) | ||||
|                     }) | ||||
|                     json.features = maprouletteFeatures | ||||
|                 } | ||||
| 
 | ||||
|                 if (json.features === undefined || json.features === null) { | ||||
|                     return; | ||||
|                     return | ||||
|                 } | ||||
| 
 | ||||
|                 if (self.layer.layerDef.source.mercatorCrs) { | ||||
|                     json = GeoOperations.GeoJsonToWGS84(json) | ||||
|                 } | ||||
| 
 | ||||
|                 const time = new Date(); | ||||
|                 const newFeatures: { feature: any, freshness: Date } [] = [] | ||||
|                 let i = 0; | ||||
|                 let skipped = 0; | ||||
|                 const time = new Date() | ||||
|                 const newFeatures: { feature: any; freshness: Date }[] = [] | ||||
|                 let i = 0 | ||||
|                 let skipped = 0 | ||||
|                 for (const feature of json.features) { | ||||
|                     const props = feature.properties | ||||
|                     for (const key in props) { | ||||
|                          | ||||
|                         if(props[key] === null){ | ||||
|                         if (props[key] === null) { | ||||
|                             delete props[key] | ||||
|                         } | ||||
| 
 | ||||
|  | @ -130,39 +126,38 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | |||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     if(self.idKey !== undefined){ | ||||
|                     if (self.idKey !== undefined) { | ||||
|                         props.id = props[self.idKey] | ||||
|                     } | ||||
| 
 | ||||
|                     if (props.id === undefined) { | ||||
|                         props.id = url + "/" + i; | ||||
|                         feature.id = url + "/" + i; | ||||
|                         i++; | ||||
|                         props.id = url + "/" + i | ||||
|                         feature.id = url + "/" + i | ||||
|                         i++ | ||||
|                     } | ||||
|                     if (self.seenids.has(props.id)) { | ||||
|                         skipped++; | ||||
|                         continue; | ||||
|                         skipped++ | ||||
|                         continue | ||||
|                     } | ||||
|                     self.seenids.add(props.id) | ||||
| 
 | ||||
|                     let freshness: Date = time; | ||||
|                     let freshness: Date = time | ||||
|                     if (feature.properties["_last_edit:timestamp"] !== undefined) { | ||||
|                         freshness = new Date(props["_last_edit:timestamp"]) | ||||
|                     } | ||||
| 
 | ||||
|                     newFeatures.push({feature: feature, freshness: freshness}) | ||||
|                     newFeatures.push({ feature: feature, freshness: freshness }) | ||||
|                 } | ||||
| 
 | ||||
|                 if (newFeatures.length == 0) { | ||||
|                     return; | ||||
|                     return | ||||
|                 } | ||||
| 
 | ||||
|                 eventSource.setData(eventSource.data.concat(newFeatures)) | ||||
| 
 | ||||
|             }).catch(msg => { | ||||
|             console.debug("Could not load geojson layer", url, "due to", msg); | ||||
|             self.state.setData({error: msg}) | ||||
|             }) | ||||
|             .catch((msg) => { | ||||
|                 console.debug("Could not load geojson layer", url, "due to", msg) | ||||
|                 self.state.setData({ error: msg }) | ||||
|             }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| import {Changes} from "../../Osm/Changes"; | ||||
| import {OsmNode, OsmObject, OsmRelation, OsmWay} from "../../Osm/OsmObject"; | ||||
| import FeatureSource from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import {ChangeDescription} from "../../Osm/Actions/ChangeDescription"; | ||||
| import {ElementStorage} from "../../ElementStorage"; | ||||
| import {OsmId, OsmTags} from "../../../Models/OsmFeature"; | ||||
| import { Changes } from "../../Osm/Changes" | ||||
| import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject" | ||||
| import FeatureSource from "../FeatureSource" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import { ChangeDescription } from "../../Osm/Actions/ChangeDescription" | ||||
| import { ElementStorage } from "../../ElementStorage" | ||||
| import { OsmId, OsmTags } from "../../../Models/OsmFeature" | ||||
| 
 | ||||
| export class NewGeometryFromChangesFeatureSource implements FeatureSource { | ||||
|     // This class name truly puts the 'Java' into 'Javascript'
 | ||||
|  | @ -15,36 +15,36 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { | |||
|      * These elements are probably created by the 'SimpleAddUi' which generates a new point, but the import functionality might create a line or polygon too. | ||||
|      * Other sources of new points are e.g. imports from nodes | ||||
|      */ | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|     public readonly name: string = "newFeatures"; | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = | ||||
|         new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||
|     public readonly name: string = "newFeatures" | ||||
| 
 | ||||
|     constructor(changes: Changes, allElementStorage: ElementStorage, backendUrl: string) { | ||||
|         const seenChanges = new Set<ChangeDescription>() | ||||
|         const features = this.features.data | ||||
|         const self = this | ||||
| 
 | ||||
|         const seenChanges = new Set<ChangeDescription>(); | ||||
|         const features = this.features.data; | ||||
|         const self = this; | ||||
| 
 | ||||
|         changes.pendingChanges.stabilized(100).addCallbackAndRunD(changes => { | ||||
|         changes.pendingChanges.stabilized(100).addCallbackAndRunD((changes) => { | ||||
|             if (changes.length === 0) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             const now = new Date(); | ||||
|             let somethingChanged = false; | ||||
|             const now = new Date() | ||||
|             let somethingChanged = false | ||||
| 
 | ||||
|             function add(feature) { | ||||
|                 feature.id = feature.properties.id | ||||
|                 features.push({ | ||||
|                     feature: feature, | ||||
|                     freshness: now | ||||
|                     freshness: now, | ||||
|                 }) | ||||
|                 somethingChanged = true; | ||||
|                 somethingChanged = true | ||||
|             } | ||||
| 
 | ||||
|             for (const change of changes) { | ||||
|                 if (seenChanges.has(change)) { | ||||
|                     // Already handled
 | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
|                 seenChanges.add(change) | ||||
| 
 | ||||
|  | @ -60,35 +60,32 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { | |||
|                     // For this, we introspect the change
 | ||||
|                     if (allElementStorage.has(change.type + "/" + change.id)) { | ||||
|                         // The current point already exists, we don't have to do anything here
 | ||||
|                         continue; | ||||
|                         continue | ||||
|                     } | ||||
|                     console.debug("Detected a reused point") | ||||
|                     // The 'allElementsStore' does _not_ have this point yet, so we have to create it
 | ||||
|                     OsmObject.DownloadObjectAsync(change.type + "/" + change.id).then(feat => { | ||||
|                     OsmObject.DownloadObjectAsync(change.type + "/" + change.id).then((feat) => { | ||||
|                         console.log("Got the reused point:", feat) | ||||
|                         for (const kv of change.tags) { | ||||
|                             feat.tags[kv.k] = kv.v | ||||
|                         } | ||||
|                         const geojson = feat.asGeoJson(); | ||||
|                         const geojson = feat.asGeoJson() | ||||
|                         allElementStorage.addOrGetElement(geojson) | ||||
|                         self.features.data.push({feature: geojson, freshness: new Date()}) | ||||
|                         self.features.data.push({ feature: geojson, freshness: new Date() }) | ||||
|                         self.features.ping() | ||||
|                     }) | ||||
|                     continue | ||||
| 
 | ||||
| 
 | ||||
|                 } else if (change.id < 0 && change.changes === undefined) { | ||||
|                     // The geometry is not described - not a new point
 | ||||
|                     if (change.id < 0) { | ||||
|                         console.error("WARNING: got a new point without geometry!") | ||||
|                     } | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 try { | ||||
|                     const tags: OsmTags = { | ||||
|                         id: <OsmId> (change.type + "/" + change.id) | ||||
|                         id: <OsmId>(change.type + "/" + change.id), | ||||
|                     } | ||||
|                     for (const kv of change.tags) { | ||||
|                         tags[kv.k] = kv.v | ||||
|  | @ -104,30 +101,31 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { | |||
|                             n.lon = change.changes["lon"] | ||||
|                             const geojson = n.asGeoJson() | ||||
|                             add(geojson) | ||||
|                             break; | ||||
|                             break | ||||
|                         case "way": | ||||
|                             const w = new OsmWay(change.id) | ||||
|                             w.tags = tags | ||||
|                             w.nodes = change.changes["nodes"] | ||||
|                             w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [lat, lon]) | ||||
|                             w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [ | ||||
|                                 lat, | ||||
|                                 lon, | ||||
|                             ]) | ||||
|                             add(w.asGeoJson()) | ||||
|                             break; | ||||
|                             break | ||||
|                         case "relation": | ||||
|                             const r = new OsmRelation(change.id) | ||||
|                             r.tags = tags | ||||
|                             r.members = change.changes["members"] | ||||
|                             add(r.asGeoJson()) | ||||
|                             break; | ||||
|                             break | ||||
|                     } | ||||
|                 } catch (e) { | ||||
|                     console.error("Could not generate a new geometry to render on screen for:", e) | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|             if (somethingChanged) { | ||||
|                 self.features.ping() | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -2,34 +2,36 @@ | |||
|  * Every previously added point is remembered, but new points are added. | ||||
|  * Data coming from upstream will always overwrite a previous value | ||||
|  */ | ||||
| import FeatureSource, {Tiled} from "../FeatureSource"; | ||||
| import {Store, UIEventSource} from "../../UIEventSource"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import FeatureSource, { Tiled } from "../FeatureSource" | ||||
| import { Store, UIEventSource } from "../../UIEventSource" | ||||
| import { BBox } from "../../BBox" | ||||
| 
 | ||||
| export default class RememberingSource implements FeatureSource, Tiled { | ||||
| 
 | ||||
|     public readonly features: Store<{ feature: any, freshness: Date }[]>; | ||||
|     public readonly name; | ||||
|     public readonly features: Store<{ feature: any; freshness: Date }[]> | ||||
|     public readonly name | ||||
|     public readonly tileIndex: number | ||||
|     public readonly bbox: BBox | ||||
| 
 | ||||
|     constructor(source: FeatureSource & Tiled) { | ||||
|         const self = this; | ||||
|         this.name = "RememberingSource of " + source.name; | ||||
|         const self = this | ||||
|         this.name = "RememberingSource of " + source.name | ||||
|         this.tileIndex = source.tileIndex | ||||
|         this.bbox = source.bbox; | ||||
|         this.bbox = source.bbox | ||||
| 
 | ||||
|         const empty = []; | ||||
|         const featureSource = new UIEventSource<{feature: any, freshness: Date}[]>(empty) | ||||
|         const empty = [] | ||||
|         const featureSource = new UIEventSource<{ feature: any; freshness: Date }[]>(empty) | ||||
|         this.features = featureSource | ||||
|         source.features.addCallbackAndRunD(features => { | ||||
|             const oldFeatures = self.features?.data ?? empty; | ||||
|         source.features.addCallbackAndRunD((features) => { | ||||
|             const oldFeatures = self.features?.data ?? empty | ||||
|             // Then new ids
 | ||||
|             const ids = new Set<string>(features.map(f => f.feature.properties.id + f.feature.geometry.type)); | ||||
|             const ids = new Set<string>( | ||||
|                 features.map((f) => f.feature.properties.id + f.feature.geometry.type) | ||||
|             ) | ||||
|             // the old data
 | ||||
|             const oldData = oldFeatures.filter(old => !ids.has(old.feature.properties.id + old.feature.geometry.type)) | ||||
|             const oldData = oldFeatures.filter( | ||||
|                 (old) => !ids.has(old.feature.properties.id + old.feature.geometry.type) | ||||
|             ) | ||||
|             featureSource.setData([...features, ...oldData]) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,50 +1,57 @@ | |||
| /** | ||||
|  * This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indicates with what renderConfig it should be rendered. | ||||
|  */ | ||||
| import {Store} from "../../UIEventSource"; | ||||
| import {GeoOperations} from "../../GeoOperations"; | ||||
| import FeatureSource from "../FeatureSource"; | ||||
| import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig"; | ||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
| import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig"; | ||||
| import { Store } from "../../UIEventSource" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| import FeatureSource from "../FeatureSource" | ||||
| import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig" | ||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||
| import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig" | ||||
| 
 | ||||
| export default class RenderingMultiPlexerFeatureSource { | ||||
|     public readonly features: Store<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>; | ||||
|     private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[]; | ||||
|     private centroidRenderings: { rendering: PointRenderingConfig; index: number }[]; | ||||
|     private projectedCentroidRenderings: { rendering: PointRenderingConfig; index: number }[]; | ||||
|     private startRenderings: { rendering: PointRenderingConfig; index: number }[]; | ||||
|     private endRenderings: { rendering: PointRenderingConfig; index: number }[]; | ||||
|     private hasCentroid: boolean; | ||||
|     private lineRenderObjects: LineRenderingConfig[]; | ||||
|     public readonly features: Store< | ||||
|         (any & { | ||||
|             pointRenderingIndex: number | undefined | ||||
|             lineRenderingIndex: number | undefined | ||||
|         })[] | ||||
|     > | ||||
|     private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[] | ||||
|     private centroidRenderings: { rendering: PointRenderingConfig; index: number }[] | ||||
|     private projectedCentroidRenderings: { rendering: PointRenderingConfig; index: number }[] | ||||
|     private startRenderings: { rendering: PointRenderingConfig; index: number }[] | ||||
|     private endRenderings: { rendering: PointRenderingConfig; index: number }[] | ||||
|     private hasCentroid: boolean | ||||
|     private lineRenderObjects: LineRenderingConfig[] | ||||
| 
 | ||||
|      | ||||
|     private inspectFeature(feat, addAsPoint: (feat, rendering, centerpoint: [number, number]) => void, withIndex: any[]){ | ||||
|     private inspectFeature( | ||||
|         feat, | ||||
|         addAsPoint: (feat, rendering, centerpoint: [number, number]) => void, | ||||
|         withIndex: any[] | ||||
|     ) { | ||||
|         if (feat.geometry.type === "Point") { | ||||
| 
 | ||||
|             for (const rendering of this.pointRenderings) { | ||||
|                 withIndex.push({ | ||||
|                     ...feat, | ||||
|                     pointRenderingIndex: rendering.index | ||||
|                     pointRenderingIndex: rendering.index, | ||||
|                 }) | ||||
|             } | ||||
|         } else { | ||||
|             // This is a a line: add the centroids
 | ||||
|             let centerpoint: [number, number] = undefined; | ||||
|             let projectedCenterPoint : [number, number] = undefined | ||||
|             if(this.hasCentroid){ | ||||
|             let centerpoint: [number, number] = undefined | ||||
|             let projectedCenterPoint: [number, number] = undefined | ||||
|             if (this.hasCentroid) { | ||||
|                 centerpoint = GeoOperations.centerpointCoordinates(feat) | ||||
|                 if(this.projectedCentroidRenderings.length > 0){ | ||||
|                     projectedCenterPoint = <[number,number]> GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates | ||||
|                 if (this.projectedCentroidRenderings.length > 0) { | ||||
|                     projectedCenterPoint = <[number, number]>( | ||||
|                         GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|             for (const rendering of this.centroidRenderings) { | ||||
|                 addAsPoint(feat, rendering, centerpoint) | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             if (feat.geometry.type === "LineString") { | ||||
| 
 | ||||
|                 for (const rendering of this.projectedCentroidRenderings) { | ||||
|                     addAsPoint(feat, rendering, projectedCenterPoint) | ||||
|                 } | ||||
|  | @ -58,8 +65,7 @@ export default class RenderingMultiPlexerFeatureSource { | |||
|                     const coordinate = coordinates[coordinates.length - 1] | ||||
|                     addAsPoint(feat, rendering, coordinate) | ||||
|                 } | ||||
| 
 | ||||
|             }else{ | ||||
|             } else { | ||||
|                 for (const rendering of this.projectedCentroidRenderings) { | ||||
|                     addAsPoint(feat, rendering, centerpoint) | ||||
|                 } | ||||
|  | @ -69,62 +75,59 @@ export default class RenderingMultiPlexerFeatureSource { | |||
|             for (let i = 0; i < this.lineRenderObjects.length; i++) { | ||||
|                 withIndex.push({ | ||||
|                     ...feat, | ||||
|                     lineRenderingIndex: i | ||||
|                     lineRenderingIndex: i, | ||||
|                 }) | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     constructor(upstream: FeatureSource, layer: LayerConfig) { | ||||
|          | ||||
|         const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({ | ||||
|         const pointRenderObjects: { rendering: PointRenderingConfig; index: number }[] = | ||||
|             layer.mapRendering.map((r, i) => ({ | ||||
|                 rendering: r, | ||||
|             index: i | ||||
|                 index: i, | ||||
|             })) | ||||
|         this.pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point")) | ||||
|         this.centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid")) | ||||
|         this.projectedCentroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("projected_centerpoint")) | ||||
|         this.startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start")) | ||||
|         this.endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end")) | ||||
|         this.hasCentroid = this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0 | ||||
|         this.pointRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("point")) | ||||
|         this.centroidRenderings = pointRenderObjects.filter((r) => | ||||
|             r.rendering.location.has("centroid") | ||||
|         ) | ||||
|         this.projectedCentroidRenderings = pointRenderObjects.filter((r) => | ||||
|             r.rendering.location.has("projected_centerpoint") | ||||
|         ) | ||||
|         this.startRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("start")) | ||||
|         this.endRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("end")) | ||||
|         this.hasCentroid = | ||||
|             this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0 | ||||
|         this.lineRenderObjects = layer.lineRendering | ||||
| 
 | ||||
|         this.features = upstream.features.map( | ||||
|             features => { | ||||
|         this.features = upstream.features.map((features) => { | ||||
|             if (features === undefined) { | ||||
|                     return undefined; | ||||
|                 return undefined | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|                 const withIndex: any[] = []; | ||||
|             const withIndex: any[] = [] | ||||
| 
 | ||||
|             function addAsPoint(feat, rendering, coordinate) { | ||||
|                 const patched = { | ||||
|                     ...feat, | ||||
|                         pointRenderingIndex: rendering.index | ||||
|                     pointRenderingIndex: rendering.index, | ||||
|                 } | ||||
|                 patched.geometry = { | ||||
|                     type: "Point", | ||||
|                         coordinates: coordinate | ||||
|                     coordinates: coordinate, | ||||
|                 } | ||||
|                 withIndex.push(patched) | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             for (const f of features) { | ||||
|                     const feat = f.feature; | ||||
|                     if(feat === undefined){ | ||||
|                 const feat = f.feature | ||||
|                 if (feat === undefined) { | ||||
|                     continue | ||||
|                 } | ||||
|                 this.inspectFeature(feat, addAsPoint, withIndex) | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|                 return withIndex; | ||||
|             return withIndex | ||||
|         }) | ||||
|     } | ||||
|         ); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,21 +1,24 @@ | |||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import { BBox } from "../../BBox" | ||||
| 
 | ||||
| export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly name: string = "SimpleFeatureSource"; | ||||
|     public readonly layer: FilteredLayer; | ||||
|     public readonly bbox: BBox = BBox.global; | ||||
|     public readonly tileIndex: number; | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> | ||||
|     public readonly name: string = "SimpleFeatureSource" | ||||
|     public readonly layer: FilteredLayer | ||||
|     public readonly bbox: BBox = BBox.global | ||||
|     public readonly tileIndex: number | ||||
| 
 | ||||
|     constructor(layer: FilteredLayer, tileIndex: number, featureSource?: UIEventSource<{ feature: any; freshness: Date }[]> ) { | ||||
|     constructor( | ||||
|         layer: FilteredLayer, | ||||
|         tileIndex: number, | ||||
|         featureSource?: UIEventSource<{ feature: any; freshness: Date }[]> | ||||
|     ) { | ||||
|         this.name = "SimpleFeatureSource(" + layer.layerDef.id + ")" | ||||
|         this.layer = layer | ||||
|         this.tileIndex = tileIndex ?? 0; | ||||
|         this.tileIndex = tileIndex ?? 0 | ||||
|         this.bbox = BBox.fromTileIndex(this.tileIndex) | ||||
|         this.features = featureSource ?? new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|         this.features = featureSource ?? new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,62 +1,90 @@ | |||
| import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {ImmutableStore, Store, UIEventSource} from "../../UIEventSource"; | ||||
| import {stat} from "fs"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import {Feature} from "@turf/turf"; | ||||
| import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" | ||||
| import { stat } from "fs" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { BBox } from "../../BBox" | ||||
| import { Feature } from "@turf/turf" | ||||
| 
 | ||||
| /** | ||||
|  * A simple, read only feature store. | ||||
|  */ | ||||
| export default class StaticFeatureSource implements FeatureSource { | ||||
|     public readonly features: Store<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly features: Store<{ feature: any; freshness: Date }[]> | ||||
|     public readonly name: string | ||||
| 
 | ||||
|     constructor(features: Store<{ feature: Feature, freshness: Date }[]>, name = "StaticFeatureSource") { | ||||
|     constructor( | ||||
|         features: Store<{ feature: Feature; freshness: Date }[]>, | ||||
|         name = "StaticFeatureSource" | ||||
|     ) { | ||||
|         if (features === undefined) { | ||||
|             throw "Static feature source received undefined as source" | ||||
|         } | ||||
|         this.name = name; | ||||
|         this.features = features; | ||||
|         this.name = name | ||||
|         this.features = features | ||||
|     } | ||||
| 
 | ||||
|     public static fromGeojsonAndDate(features: { feature: Feature, freshness: Date }[], name = "StaticFeatureSourceFromGeojsonAndDate"): StaticFeatureSource { | ||||
|         return new StaticFeatureSource(new ImmutableStore(features), name); | ||||
|     public static fromGeojsonAndDate( | ||||
|         features: { feature: Feature; freshness: Date }[], | ||||
|         name = "StaticFeatureSourceFromGeojsonAndDate" | ||||
|     ): StaticFeatureSource { | ||||
|         return new StaticFeatureSource(new ImmutableStore(features), name) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static fromGeojson(geojson: Feature[], name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource { | ||||
|         const now = new Date(); | ||||
|         return StaticFeatureSource.fromGeojsonAndDate(geojson.map(feature => ({feature, freshness: now})), name); | ||||
|     public static fromGeojson( | ||||
|         geojson: Feature[], | ||||
|         name = "StaticFeatureSourceFromGeojson" | ||||
|     ): StaticFeatureSource { | ||||
|         const now = new Date() | ||||
|         return StaticFeatureSource.fromGeojsonAndDate( | ||||
|             geojson.map((feature) => ({ feature, freshness: now })), | ||||
|             name | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     public static fromGeojsonStore(geojson: Store<Feature[]>, name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource { | ||||
|         const now = new Date(); | ||||
|         const mapped : Store<{feature: Feature, freshness: Date}[]> = geojson.map(features => features.map(feature => ({feature, freshness: now}))) | ||||
|         return new StaticFeatureSource(mapped, name); | ||||
|     public static fromGeojsonStore( | ||||
|         geojson: Store<Feature[]>, | ||||
|         name = "StaticFeatureSourceFromGeojson" | ||||
|     ): StaticFeatureSource { | ||||
|         const now = new Date() | ||||
|         const mapped: Store<{ feature: Feature; freshness: Date }[]> = geojson.map((features) => | ||||
|             features.map((feature) => ({ feature, freshness: now })) | ||||
|         ) | ||||
|         return new StaticFeatureSource(mapped, name) | ||||
|     } | ||||
| 
 | ||||
|     static fromDateless(featureSource: Store<{ feature: Feature }[]>, name = "StaticFeatureSourceFromDateless") { | ||||
|         const now = new Date(); | ||||
|         return new StaticFeatureSource(featureSource.map(features => features.map(feature => ({ | ||||
|     static fromDateless( | ||||
|         featureSource: Store<{ feature: Feature }[]>, | ||||
|         name = "StaticFeatureSourceFromDateless" | ||||
|     ) { | ||||
|         const now = new Date() | ||||
|         return new StaticFeatureSource( | ||||
|             featureSource.map((features) => | ||||
|                 features.map((feature) => ({ | ||||
|                     feature: feature.feature, | ||||
|             freshness: now | ||||
|         }))), name); | ||||
|                     freshness: now, | ||||
|                 })) | ||||
|             ), | ||||
|             name | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class TiledStaticFeatureSource extends StaticFeatureSource implements Tiled, FeatureSourceForLayer{ | ||||
| export class TiledStaticFeatureSource | ||||
|     extends StaticFeatureSource | ||||
|     implements Tiled, FeatureSourceForLayer | ||||
| { | ||||
|     public readonly bbox: BBox = BBox.global | ||||
|     public readonly tileIndex: number | ||||
|     public readonly layer: FilteredLayer | ||||
| 
 | ||||
|     public readonly bbox: BBox = BBox.global; | ||||
|     public readonly tileIndex: number;    | ||||
|     public readonly layer: FilteredLayer; | ||||
| 
 | ||||
|     constructor(features: Store<{ feature: any, freshness: Date }[]>, layer: FilteredLayer ,tileIndex : number = 0) { | ||||
|         super(features); | ||||
|         this.tileIndex = tileIndex ; | ||||
|         this.layer=  layer; | ||||
|     constructor( | ||||
|         features: Store<{ feature: any; freshness: Date }[]>, | ||||
|         layer: FilteredLayer, | ||||
|         tileIndex: number = 0 | ||||
|     ) { | ||||
|         super(features) | ||||
|         this.tileIndex = tileIndex | ||||
|         this.layer = layer | ||||
|         this.bbox = BBox.fromTileIndex(this.tileIndex) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,12 +1,11 @@ | |||
| import {Tiles} from "../../Models/TileRange"; | ||||
| import { Tiles } from "../../Models/TileRange" | ||||
| 
 | ||||
| export default class TileFreshnessCalculator { | ||||
| 
 | ||||
|     /** | ||||
|      * All the freshnesses per tile index | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly freshnesses = new Map<number, Date>(); | ||||
|     private readonly freshnesses = new Map<number, Date>() | ||||
| 
 | ||||
|     /** | ||||
|      * Marks that some data got loaded for this layer | ||||
|  | @ -16,14 +15,14 @@ export default class TileFreshnessCalculator { | |||
|     public addTileLoad(tileId: number, freshness: Date) { | ||||
|         const existingFreshness = this.freshnessFor(...Tiles.tile_from_index(tileId)) | ||||
|         if (existingFreshness >= freshness) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         this.freshnesses.set(tileId, freshness) | ||||
| 
 | ||||
|         // Do we have freshness for the neighbouring tiles? If so, we can mark the tile above as loaded too!
 | ||||
|         let [z, x, y] = Tiles.tile_from_index(tileId) | ||||
|         if (z === 0) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         x = x - (x % 2) // Make the tiles always even
 | ||||
|         y = y - (y % 2) | ||||
|  | @ -48,11 +47,7 @@ export default class TileFreshnessCalculator { | |||
|         const leastFresh = Math.min(ul, ur, ll, lr) | ||||
|         const date = new Date() | ||||
|         date.setTime(leastFresh) | ||||
|         this.addTileLoad( | ||||
|             Tiles.tile_index(z - 1, Math.floor(x / 2), Math.floor(y / 2)), | ||||
|             date | ||||
|         ) | ||||
| 
 | ||||
|         this.addTileLoad(Tiles.tile_index(z - 1, Math.floor(x / 2), Math.floor(y / 2)), date) | ||||
|     } | ||||
| 
 | ||||
|     public freshnessFor(z: number, x: number, y: number): Date { | ||||
|  | @ -65,7 +60,5 @@ export default class TileFreshnessCalculator { | |||
|         } | ||||
|         // recurse up
 | ||||
|         return this.freshnessFor(z - 1, Math.floor(x / 2), Math.floor(y / 2)) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,21 +1,22 @@ | |||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import DynamicTileSource from "./DynamicTileSource"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import GeoJsonSource from "../Sources/GeoJsonSource"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import DynamicTileSource from "./DynamicTileSource" | ||||
| import { Utils } from "../../../Utils" | ||||
| import GeoJsonSource from "../Sources/GeoJsonSource" | ||||
| import { BBox } from "../../BBox" | ||||
| 
 | ||||
| export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||
| 
 | ||||
|     private static whitelistCache = new Map<string, any>() | ||||
| 
 | ||||
|     constructor(layer: FilteredLayer, | ||||
|     constructor( | ||||
|         layer: FilteredLayer, | ||||
|         registerLayer: (layer: FeatureSourceForLayer & Tiled) => void, | ||||
|         state: { | ||||
|                     locationControl?: UIEventSource<{zoom?: number}> | ||||
|             locationControl?: UIEventSource<{ zoom?: number }> | ||||
|             currentBounds: UIEventSource<BBox> | ||||
|                 }) { | ||||
|         } | ||||
|     ) { | ||||
|         const source = layer.layerDef.source | ||||
|         if (source.geojsonZoomLevel === undefined) { | ||||
|             throw "Invalid layer: geojsonZoomLevel expected" | ||||
|  | @ -26,7 +27,6 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | |||
| 
 | ||||
|         let whitelist = undefined | ||||
|         if (source.geojsonSource.indexOf("{x}_{y}.geojson") > 0) { | ||||
| 
 | ||||
|             const whitelistUrl = source.geojsonSource | ||||
|                 .replace("{z}", "" + source.geojsonZoomLevel) | ||||
|                 .replace("{x}_{y}.geojson", "overview.json") | ||||
|  | @ -35,26 +35,33 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | |||
|             if (DynamicGeoJsonTileSource.whitelistCache.has(whitelistUrl)) { | ||||
|                 whitelist = DynamicGeoJsonTileSource.whitelistCache.get(whitelistUrl) | ||||
|             } else { | ||||
|                 Utils.downloadJsonCached(whitelistUrl, 1000 * 60 * 60).then( | ||||
|                     json => { | ||||
|                         const data = new Map<number, Set<number>>(); | ||||
|                 Utils.downloadJsonCached(whitelistUrl, 1000 * 60 * 60) | ||||
|                     .then((json) => { | ||||
|                         const data = new Map<number, Set<number>>() | ||||
|                         for (const x in json) { | ||||
|                             if (x === "zoom") { | ||||
|                                 continue | ||||
|                             } | ||||
|                             data.set(Number(x), new Set(json[x])) | ||||
|                         } | ||||
|                         console.log("The whitelist is", data, "based on ", json, "from", whitelistUrl) | ||||
|                         console.log( | ||||
|                             "The whitelist is", | ||||
|                             data, | ||||
|                             "based on ", | ||||
|                             json, | ||||
|                             "from", | ||||
|                             whitelistUrl | ||||
|                         ) | ||||
|                         whitelist = data | ||||
|                         DynamicGeoJsonTileSource.whitelistCache.set(whitelistUrl, whitelist) | ||||
|                     } | ||||
|                 ).catch(err => { | ||||
|                     }) | ||||
|                     .catch((err) => { | ||||
|                         console.warn("No whitelist found for ", layer.layerDef.id, err) | ||||
|                     }) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const blackList = (new Set<string>()) | ||||
|         const blackList = new Set<string>() | ||||
|         super( | ||||
|             layer, | ||||
|             source.geojsonZoomLevel, | ||||
|  | @ -62,29 +69,28 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | |||
|                 if (whitelist !== undefined) { | ||||
|                     const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2]) | ||||
|                     if (!isWhiteListed) { | ||||
|                         console.debug("Not downloading tile", ...zxy, "as it is not on the whitelist") | ||||
|                         return undefined; | ||||
|                         console.debug( | ||||
|                             "Not downloading tile", | ||||
|                             ...zxy, | ||||
|                             "as it is not on the whitelist" | ||||
|                         ) | ||||
|                         return undefined | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 const src = new GeoJsonSource( | ||||
|                     layer, | ||||
|                     zxy, | ||||
|                     { | ||||
|                         featureIdBlacklist: blackList | ||||
|                     } | ||||
|                 ) | ||||
|                 const src = new GeoJsonSource(layer, zxy, { | ||||
|                     featureIdBlacklist: blackList, | ||||
|                 }) | ||||
| 
 | ||||
|                 registerLayer(src) | ||||
|                 return src | ||||
|             }, | ||||
|             state | ||||
|         ); | ||||
| 
 | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     public static RegisterWhitelist(url: string, json: any) { | ||||
|         const data = new Map<number, Set<number>>(); | ||||
|         const data = new Map<number, Set<number>>() | ||||
|         for (const x in json) { | ||||
|             if (x === "zoom") { | ||||
|                 continue | ||||
|  | @ -93,5 +99,4 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | |||
|         } | ||||
|         DynamicGeoJsonTileSource.whitelistCache.set(url, data) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,31 +1,32 @@ | |||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import TileHierarchy from "./TileHierarchy"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import TileHierarchy from "./TileHierarchy" | ||||
| import { Tiles } from "../../../Models/TileRange" | ||||
| import { BBox } from "../../BBox" | ||||
| 
 | ||||
| /*** | ||||
|  * A tiled source which dynamically loads the required tiles at a fixed zoom level | ||||
|  */ | ||||
| export default class DynamicTileSource implements TileHierarchy<FeatureSourceForLayer & Tiled> { | ||||
|     public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>; | ||||
|     private readonly _loadedTiles = new Set<number>(); | ||||
|     public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> | ||||
|     private readonly _loadedTiles = new Set<number>() | ||||
| 
 | ||||
|     constructor( | ||||
|         layer: FilteredLayer, | ||||
|         zoomlevel: number, | ||||
|         constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled), | ||||
|         constructTile: (zxy: [number, number, number]) => FeatureSourceForLayer & Tiled, | ||||
|         state: { | ||||
|             currentBounds: UIEventSource<BBox>; | ||||
|             locationControl?: UIEventSource<{zoom?: number}> | ||||
|             currentBounds: UIEventSource<BBox> | ||||
|             locationControl?: UIEventSource<{ zoom?: number }> | ||||
|         } | ||||
|     ) { | ||||
|         const self = this; | ||||
|         const self = this | ||||
| 
 | ||||
|         this.loadedTiles = new Map<number, FeatureSourceForLayer & Tiled>() | ||||
|         const neededTiles = state.currentBounds.map( | ||||
|             bounds => { | ||||
|         const neededTiles = state.currentBounds | ||||
|             .map( | ||||
|                 (bounds) => { | ||||
|                     if (bounds === undefined) { | ||||
|                         // We'll retry later
 | ||||
|                         return undefined | ||||
|  | @ -33,32 +34,47 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor | |||
| 
 | ||||
|                     if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) { | ||||
|                         // No need to download! - the layer is disabled
 | ||||
|                     return undefined; | ||||
|                 } | ||||
| 
 | ||||
|                 if (state.locationControl?.data?.zoom !== undefined && state.locationControl.data.zoom < layer.layerDef.minzoom) { | ||||
|                     // No need to download! - the layer is disabled
 | ||||
|                     return undefined; | ||||
|                 } | ||||
| 
 | ||||
|                 const tileRange = Tiles.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) | ||||
|                 if (tileRange.total > 10000) { | ||||
|                     console.error("Got a really big tilerange, bounds and location might be out of sync") | ||||
|                         return undefined | ||||
|                     } | ||||
| 
 | ||||
|                 const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i)) | ||||
|                     if ( | ||||
|                         state.locationControl?.data?.zoom !== undefined && | ||||
|                         state.locationControl.data.zoom < layer.layerDef.minzoom | ||||
|                     ) { | ||||
|                         // No need to download! - the layer is disabled
 | ||||
|                         return undefined | ||||
|                     } | ||||
| 
 | ||||
|                     const tileRange = Tiles.TileRangeBetween( | ||||
|                         zoomlevel, | ||||
|                         bounds.getNorth(), | ||||
|                         bounds.getEast(), | ||||
|                         bounds.getSouth(), | ||||
|                         bounds.getWest() | ||||
|                     ) | ||||
|                     if (tileRange.total > 10000) { | ||||
|                         console.error( | ||||
|                             "Got a really big tilerange, bounds and location might be out of sync" | ||||
|                         ) | ||||
|                         return undefined | ||||
|                     } | ||||
| 
 | ||||
|                     const needed = Tiles.MapRange(tileRange, (x, y) => | ||||
|                         Tiles.tile_index(zoomlevel, x, y) | ||||
|                     ).filter((i) => !self._loadedTiles.has(i)) | ||||
|                     if (needed.length === 0) { | ||||
|                         return undefined | ||||
|                     } | ||||
|                     return needed | ||||
|             } | ||||
|             , [layer.isDisplayed, state.locationControl]).stabilized(250); | ||||
|                 }, | ||||
|                 [layer.isDisplayed, state.locationControl] | ||||
|             ) | ||||
|             .stabilized(250) | ||||
| 
 | ||||
|         neededTiles.addCallbackAndRunD(neededIndexes => { | ||||
|         neededTiles.addCallbackAndRunD((neededIndexes) => { | ||||
|             console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes) | ||||
|             if (neededIndexes === undefined) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             for (const neededIndex of neededIndexes) { | ||||
|                 self._loadedTiles.add(neededIndex) | ||||
|  | @ -68,10 +84,5 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor | |||
|                 } | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,30 +1,26 @@ | |||
| import TileHierarchy from "./TileHierarchy"; | ||||
| import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject"; | ||||
| import SimpleFeatureSource from "../Sources/SimpleFeatureSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| 
 | ||||
| import TileHierarchy from "./TileHierarchy" | ||||
| import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject" | ||||
| import SimpleFeatureSource from "../Sources/SimpleFeatureSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| 
 | ||||
| export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> { | ||||
|     public readonly loadedTiles = new Map<number, FeatureSource & Tiled>() | ||||
|     private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void; | ||||
|     private readonly onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void | ||||
|     private readonly layer: FilteredLayer | ||||
|     private readonly nodeByIds = new Map<number, OsmNode>(); | ||||
|     private readonly nodeByIds = new Map<number, OsmNode>() | ||||
|     private readonly parentWays = new Map<number, UIEventSource<OsmWay[]>>() | ||||
| 
 | ||||
|     constructor( | ||||
|         layer: FilteredLayer, | ||||
|         onTileLoaded: ((tile: Tiled & FeatureSourceForLayer) => void)) { | ||||
|     constructor(layer: FilteredLayer, onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void) { | ||||
|         this.onTileLoaded = onTileLoaded | ||||
|         this.layer = layer; | ||||
|         this.layer = layer | ||||
|         if (this.layer === undefined) { | ||||
|             throw "Layer is undefined" | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public handleOsmJson(osmJson: any, tileId: number) { | ||||
| 
 | ||||
|         const allObjects = OsmObject.ParseObjects(osmJson.elements) | ||||
|         const nodesById = new Map<number, OsmNode>() | ||||
| 
 | ||||
|  | @ -32,7 +28,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour | |||
|             if (osmObj.type !== "node") { | ||||
|                 continue | ||||
|             } | ||||
|             const osmNode = <OsmNode>osmObj; | ||||
|             const osmNode = <OsmNode>osmObj | ||||
|             nodesById.set(osmNode.id, osmNode) | ||||
|             this.nodeByIds.set(osmNode.id, osmNode) | ||||
|         } | ||||
|  | @ -41,33 +37,32 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour | |||
|             if (osmObj.type !== "way") { | ||||
|                 continue | ||||
|             } | ||||
|             const osmWay = <OsmWay>osmObj; | ||||
|             const osmWay = <OsmWay>osmObj | ||||
|             for (const nodeId of osmWay.nodes) { | ||||
| 
 | ||||
|                 if (!this.parentWays.has(nodeId)) { | ||||
|                     const src = new UIEventSource<OsmWay[]>([]) | ||||
|                     this.parentWays.set(nodeId, src) | ||||
|                     src.addCallback(parentWays => { | ||||
|                     src.addCallback((parentWays) => { | ||||
|                         const tgs = nodesById.get(nodeId).tags | ||||
|                         tgs    ["parent_ways"] = JSON.stringify(parentWays.map(w => w.tags)) | ||||
|                         tgs["parent_way_ids"] = JSON.stringify(parentWays.map(w => w.id)) | ||||
|                         tgs["parent_ways"] = JSON.stringify(parentWays.map((w) => w.tags)) | ||||
|                         tgs["parent_way_ids"] = JSON.stringify(parentWays.map((w) => w.id)) | ||||
|                     }) | ||||
|                 } | ||||
|                 const src = this.parentWays.get(nodeId) | ||||
|                 src.data.push(osmWay) | ||||
|                 src.ping(); | ||||
|                 src.ping() | ||||
|             } | ||||
|         } | ||||
|         const now = new Date() | ||||
|         const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({ | ||||
|             feature: osmNode.asGeoJson(), freshness: now | ||||
|         const asGeojsonFeatures = Array.from(nodesById.values()).map((osmNode) => ({ | ||||
|             feature: osmNode.asGeoJson(), | ||||
|             freshness: now, | ||||
|         })) | ||||
| 
 | ||||
|         const featureSource = new SimpleFeatureSource(this.layer, tileId) | ||||
|         featureSource.features.setData(asGeojsonFeatures) | ||||
|         this.loadedTiles.set(tileId, featureSource) | ||||
|         this.onTileLoaded(featureSource) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -88,6 +83,4 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour | |||
|     public GetParentWays(nodeId: number): UIEventSource<OsmWay[]> { | ||||
|         return this.parentWays.get(nodeId) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,17 +1,17 @@ | |||
| import {Utils} from "../../../Utils"; | ||||
| import * as OsmToGeoJson from "osmtogeojson"; | ||||
| import StaticFeatureSource from "../Sources/StaticFeatureSource"; | ||||
| import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter"; | ||||
| import {Store, UIEventSource} from "../../UIEventSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {Or} from "../../Tags/Or"; | ||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | ||||
| import {OsmObject} from "../../Osm/OsmObject"; | ||||
| import {FeatureCollection} from "@turf/turf"; | ||||
| import { Utils } from "../../../Utils" | ||||
| import * as OsmToGeoJson from "osmtogeojson" | ||||
| import StaticFeatureSource from "../Sources/StaticFeatureSource" | ||||
| import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter" | ||||
| import { Store, UIEventSource } from "../../UIEventSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import { Tiles } from "../../../Models/TileRange" | ||||
| import { BBox } from "../../BBox" | ||||
| import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" | ||||
| import { Or } from "../../Tags/Or" | ||||
| import { TagsFilter } from "../../Tags/TagsFilter" | ||||
| import { OsmObject } from "../../Osm/OsmObject" | ||||
| import { FeatureCollection } from "@turf/turf" | ||||
| 
 | ||||
| /** | ||||
|  * If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile' | ||||
|  | @ -20,67 +20,70 @@ export default class OsmFeatureSource { | |||
|     public readonly isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
|     public readonly downloadedTiles = new Set<number>() | ||||
|     public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = [] | ||||
|     private readonly _backend: string; | ||||
|     private readonly filteredLayers: Store<FilteredLayer[]>; | ||||
|     private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void; | ||||
|     private isActive: Store<boolean>; | ||||
|     private readonly _backend: string | ||||
|     private readonly filteredLayers: Store<FilteredLayer[]> | ||||
|     private readonly handleTile: (fs: FeatureSourceForLayer & Tiled) => void | ||||
|     private isActive: Store<boolean> | ||||
|     private options: { | ||||
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void; | ||||
|         isActive: Store<boolean>, | ||||
|         neededTiles: Store<number[]>, | ||||
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void | ||||
|         isActive: Store<boolean> | ||||
|         neededTiles: Store<number[]> | ||||
|         markTileVisited?: (tileId: number) => void | ||||
|     }; | ||||
|     private readonly allowedTags: TagsFilter; | ||||
|     } | ||||
|     private readonly allowedTags: TagsFilter | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|      * @param options: allowedFeatures is normally calculated from the layoutToUse | ||||
|      */ | ||||
|     constructor(options: { | ||||
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void; | ||||
|         isActive: Store<boolean>, | ||||
|         neededTiles: Store<number[]>, | ||||
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void | ||||
|         isActive: Store<boolean> | ||||
|         neededTiles: Store<number[]> | ||||
|         state: { | ||||
|             readonly filteredLayers: UIEventSource<FilteredLayer[]>; | ||||
|             readonly filteredLayers: UIEventSource<FilteredLayer[]> | ||||
|             readonly osmConnection: { | ||||
|                 Backend(): string | ||||
|             }; | ||||
|             } | ||||
|             readonly layoutToUse?: LayoutConfig | ||||
|         }, | ||||
|         readonly allowedFeatures?: TagsFilter, | ||||
|         } | ||||
|         readonly allowedFeatures?: TagsFilter | ||||
|         markTileVisited?: (tileId: number) => void | ||||
|     }) { | ||||
|         this.options = options; | ||||
|         this._backend = options.state.osmConnection.Backend(); | ||||
|         this.filteredLayers = options.state.filteredLayers.map(layers => layers.filter(layer => layer.layerDef.source.geojsonSource === undefined)) | ||||
|         this.options = options | ||||
|         this._backend = options.state.osmConnection.Backend() | ||||
|         this.filteredLayers = options.state.filteredLayers.map((layers) => | ||||
|             layers.filter((layer) => layer.layerDef.source.geojsonSource === undefined) | ||||
|         ) | ||||
|         this.handleTile = options.handleTile | ||||
|         this.isActive = options.isActive | ||||
|         const self = this | ||||
|         options.neededTiles.addCallbackAndRunD(neededTiles => { | ||||
|         options.neededTiles.addCallbackAndRunD((neededTiles) => { | ||||
|             self.Update(neededTiles) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         const neededLayers = (options.state.layoutToUse?.layers ?? []) | ||||
|             .filter(layer => !layer.doNotDownload) | ||||
|             .filter(layer => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer) | ||||
|         this.allowedTags = options.allowedFeatures ?? new Or(neededLayers.map(l => l.source.osmTags)) | ||||
|             .filter((layer) => !layer.doNotDownload) | ||||
|             .filter( | ||||
|                 (layer) => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer | ||||
|             ) | ||||
|         this.allowedTags = | ||||
|             options.allowedFeatures ?? new Or(neededLayers.map((l) => l.source.osmTags)) | ||||
|     } | ||||
| 
 | ||||
|     private async Update(neededTiles: number[]) { | ||||
|         if (this.options.isActive?.data === false) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         neededTiles = neededTiles.filter(tile => !this.downloadedTiles.has(tile)) | ||||
|         neededTiles = neededTiles.filter((tile) => !this.downloadedTiles.has(tile)) | ||||
| 
 | ||||
|         if (neededTiles.length == 0) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         this.isRunning.setData(true) | ||||
|         try { | ||||
| 
 | ||||
|             for (const neededTile of neededTiles) { | ||||
|                 this.downloadedTiles.add(neededTile) | ||||
|                 await this.LoadTile(...Tiles.tile_from_index(neededTile)) | ||||
|  | @ -98,24 +101,30 @@ export default class OsmFeatureSource { | |||
|      * This method will download the full relation and return it as geojson if it was incomplete. | ||||
|      * If the feature is already complete (or is not a relation), the feature will be returned | ||||
|      */ | ||||
|     private async patchIncompleteRelations(feature: {properties: {id: string}},  | ||||
|                                            originalJson: {elements: {type: "node" | "way" | "relation", id: number, } []}): Promise<any> { | ||||
|         if(!feature.properties.id.startsWith("relation")){ | ||||
|     private async patchIncompleteRelations( | ||||
|         feature: { properties: { id: string } }, | ||||
|         originalJson: { elements: { type: "node" | "way" | "relation"; id: number }[] } | ||||
|     ): Promise<any> { | ||||
|         if (!feature.properties.id.startsWith("relation")) { | ||||
|             return feature | ||||
|         } | ||||
|         const relationSpec = originalJson.elements.find(f => "relation/"+f.id === feature.properties.id) | ||||
|         const members : {type: string, ref: number}[] = relationSpec["members"] | ||||
|         const relationSpec = originalJson.elements.find( | ||||
|             (f) => "relation/" + f.id === feature.properties.id | ||||
|         ) | ||||
|         const members: { type: string; ref: number }[] = relationSpec["members"] | ||||
|         for (const member of members) { | ||||
|             const isFound = originalJson.elements.some(f => f.id === member.ref && f.type === member.type) | ||||
|             const isFound = originalJson.elements.some( | ||||
|                 (f) => f.id === member.ref && f.type === member.type | ||||
|             ) | ||||
|             if (isFound) { | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             // This member is missing. We redownload the entire relation instead
 | ||||
|             console.debug("Fetching incomplete relation "+feature.properties.id) | ||||
|             console.debug("Fetching incomplete relation " + feature.properties.id) | ||||
|             return (await OsmObject.DownloadObjectAsync(feature.properties.id)).asGeoJson() | ||||
|         } | ||||
|         return feature; | ||||
|         return feature | ||||
|     } | ||||
| 
 | ||||
|     private async LoadTile(z, x, y): Promise<void> { | ||||
|  | @ -130,52 +139,69 @@ export default class OsmFeatureSource { | |||
|         const bbox = BBox.fromTile(z, x, y) | ||||
|         const url = `${this._backend}/api/0.6/map?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` | ||||
| 
 | ||||
|         let error = undefined; | ||||
|         let error = undefined | ||||
|         try { | ||||
|             const osmJson = await Utils.downloadJson(url) | ||||
|             try { | ||||
| 
 | ||||
|                 console.log("Got tile", z, x, y, "from the osm api") | ||||
|                 this.rawDataHandlers.forEach(handler => handler(osmJson, Tiles.tile_index(z, x, y))) | ||||
|                 const geojson = <FeatureCollection<any , {id: string}>> OsmToGeoJson.default(osmJson, | ||||
|                 this.rawDataHandlers.forEach((handler) => | ||||
|                     handler(osmJson, Tiles.tile_index(z, x, y)) | ||||
|                 ) | ||||
|                 const geojson = <FeatureCollection<any, { id: string }>>OsmToGeoJson.default( | ||||
|                     osmJson, | ||||
|                     // @ts-ignore
 | ||||
|                     { | ||||
|                         flatProperties: true | ||||
|                     }); | ||||
| 
 | ||||
|                         flatProperties: true, | ||||
|                     } | ||||
|                 ) | ||||
| 
 | ||||
|                 // The geojson contains _all_ features at the given location
 | ||||
|                 // We only keep what is needed
 | ||||
| 
 | ||||
|                 geojson.features = geojson.features.filter(feature => this.allowedTags.matchesProperties(feature.properties)) | ||||
|                 geojson.features = geojson.features.filter((feature) => | ||||
|                     this.allowedTags.matchesProperties(feature.properties) | ||||
|                 ) | ||||
| 
 | ||||
|                 for (let i = 0; i < geojson.features.length; i++) { | ||||
|                     geojson.features[i] = await this.patchIncompleteRelations(geojson.features[i], osmJson) | ||||
|                     geojson.features[i] = await this.patchIncompleteRelations( | ||||
|                         geojson.features[i], | ||||
|                         osmJson | ||||
|                     ) | ||||
|                 } | ||||
|                 geojson.features.forEach(f => { | ||||
|                 geojson.features.forEach((f) => { | ||||
|                     f.properties["_backend"] = this._backend | ||||
|                 }) | ||||
| 
 | ||||
|                 const index = Tiles.tile_index(z, x, y); | ||||
|                 new PerLayerFeatureSourceSplitter(this.filteredLayers, | ||||
|                 const index = Tiles.tile_index(z, x, y) | ||||
|                 new PerLayerFeatureSourceSplitter( | ||||
|                     this.filteredLayers, | ||||
|                     this.handleTile, | ||||
|                     StaticFeatureSource.fromGeojson(geojson.features), | ||||
|                     { | ||||
|                         tileIndex: index | ||||
|                         tileIndex: index, | ||||
|                     } | ||||
|                 ); | ||||
|                 ) | ||||
|                 if (this.options.markTileVisited) { | ||||
|                     this.options.markTileVisited(index) | ||||
|                 } | ||||
|             }catch(e){ | ||||
|                 console.error("PANIC: got the tile from the OSM-api, but something crashed handling this tile") | ||||
|                 error = e; | ||||
|             } | ||||
|              | ||||
|             } catch (e) { | ||||
|             console.error("Could not download tile", z, x, y, "due to", e, "; retrying with smaller bounds") | ||||
|                 console.error( | ||||
|                     "PANIC: got the tile from the OSM-api, but something crashed handling this tile" | ||||
|                 ) | ||||
|                 error = e | ||||
|             } | ||||
|         } catch (e) { | ||||
|             console.error( | ||||
|                 "Could not download tile", | ||||
|                 z, | ||||
|                 x, | ||||
|                 y, | ||||
|                 "due to", | ||||
|                 e, | ||||
|                 "; retrying with smaller bounds" | ||||
|             ) | ||||
|             if (e === "rate limited") { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             await this.LoadTile(z + 1, x * 2, y * 2) | ||||
|             await this.LoadTile(z + 1, 1 + x * 2, y * 2) | ||||
|  | @ -183,10 +209,8 @@ export default class OsmFeatureSource { | |||
|             await this.LoadTile(z + 1, 1 + x * 2, 1 + y * 2) | ||||
|         } | ||||
| 
 | ||||
|         if(error !== undefined){ | ||||
|             throw error; | ||||
|         if (error !== undefined) { | ||||
|             throw error | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,25 +1,24 @@ | |||
| import FeatureSource, {Tiled} from "../FeatureSource"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import FeatureSource, { Tiled } from "../FeatureSource" | ||||
| import { BBox } from "../../BBox" | ||||
| 
 | ||||
| export default interface TileHierarchy<T extends FeatureSource & Tiled> { | ||||
| 
 | ||||
|     /** | ||||
|      * A mapping from 'tile_index' to the actual tile featrues | ||||
|      */ | ||||
|     loadedTiles: Map<number, T> | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class TileHierarchyTools { | ||||
| 
 | ||||
|     public static getTiles<T extends FeatureSource & Tiled>(hierarchy: TileHierarchy<T>, bbox: BBox): T[] { | ||||
|     public static getTiles<T extends FeatureSource & Tiled>( | ||||
|         hierarchy: TileHierarchy<T>, | ||||
|         bbox: BBox | ||||
|     ): T[] { | ||||
|         const result: T[] = [] | ||||
|         hierarchy.loadedTiles.forEach((tile) => { | ||||
|             if (tile.bbox.overlapsWith(bbox)) { | ||||
|                 result.push(tile) | ||||
|             } | ||||
|         }) | ||||
|         return result; | ||||
|         return result | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,20 +1,32 @@ | |||
| import TileHierarchy from "./TileHierarchy"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import FeatureSourceMerger from "../Sources/FeatureSourceMerger"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import TileHierarchy from "./TileHierarchy" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import FeatureSourceMerger from "../Sources/FeatureSourceMerger" | ||||
| import { Tiles } from "../../../Models/TileRange" | ||||
| import { BBox } from "../../BBox" | ||||
| 
 | ||||
| export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> { | ||||
|     public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>(); | ||||
|     public readonly layer: FilteredLayer; | ||||
|     private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<number, UIEventSource<FeatureSource[]>>(); | ||||
|     private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void; | ||||
|     public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map< | ||||
|         number, | ||||
|         FeatureSourceForLayer & Tiled | ||||
|     >() | ||||
|     public readonly layer: FilteredLayer | ||||
|     private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map< | ||||
|         number, | ||||
|         UIEventSource<FeatureSource[]> | ||||
|     >() | ||||
|     private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void | ||||
| 
 | ||||
|     constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, index: number) => void) { | ||||
|         this.layer = layer; | ||||
|         this._handleTile = handleTile; | ||||
|     constructor( | ||||
|         layer: FilteredLayer, | ||||
|         handleTile: ( | ||||
|             src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, | ||||
|             index: number | ||||
|         ) => void | ||||
|     ) { | ||||
|         this.layer = layer | ||||
|         this._handleTile = handleTile | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -23,22 +35,24 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer | |||
|      * @param src | ||||
|      */ | ||||
|     public registerTile(src: FeatureSource & Tiled) { | ||||
| 
 | ||||
|         const index = src.tileIndex | ||||
|         if (this.sources.has(index)) { | ||||
|             const sources = this.sources.get(index) | ||||
|             sources.data.push(src) | ||||
|             sources.ping() | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         // We have to setup
 | ||||
|         const sources = new UIEventSource<FeatureSource[]>([src]) | ||||
|         this.sources.set(index, sources) | ||||
|         const merger = new FeatureSourceMerger(this.layer, index, BBox.fromTile(...Tiles.tile_from_index(index)), sources) | ||||
|         const merger = new FeatureSourceMerger( | ||||
|             this.layer, | ||||
|             index, | ||||
|             BBox.fromTile(...Tiles.tile_from_index(index)), | ||||
|             sources | ||||
|         ) | ||||
|         this.loadedTiles.set(index, merger) | ||||
|         this._handleTile(merger, index) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,53 +1,65 @@ | |||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; | ||||
| import {Store, UIEventSource} from "../../UIEventSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import TileHierarchy from "./TileHierarchy"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" | ||||
| import { Store, UIEventSource } from "../../UIEventSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import TileHierarchy from "./TileHierarchy" | ||||
| import { Tiles } from "../../../Models/TileRange" | ||||
| import { BBox } from "../../BBox" | ||||
| 
 | ||||
| /** | ||||
|  * Contains all features in a tiled fashion. | ||||
|  * The data will be automatically broken down into subtiles when there are too much features in a single tile or if the zoomlevel is too high | ||||
|  */ | ||||
| export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, FeatureSourceForLayer, TileHierarchy<IndexedFeatureSource & FeatureSourceForLayer & Tiled> { | ||||
|     public readonly z: number; | ||||
|     public readonly x: number; | ||||
|     public readonly y: number; | ||||
|     public readonly parent: TiledFeatureSource; | ||||
| export default class TiledFeatureSource | ||||
|     implements | ||||
|         Tiled, | ||||
|         IndexedFeatureSource, | ||||
|         FeatureSourceForLayer, | ||||
|         TileHierarchy<IndexedFeatureSource & FeatureSourceForLayer & Tiled> | ||||
| { | ||||
|     public readonly z: number | ||||
|     public readonly x: number | ||||
|     public readonly y: number | ||||
|     public readonly parent: TiledFeatureSource | ||||
|     public readonly root: TiledFeatureSource | ||||
|     public readonly layer: FilteredLayer; | ||||
|     public readonly layer: FilteredLayer | ||||
|     /* An index of all known tiles. allTiles[z][x][y].get('layerid') will yield the corresponding tile. | ||||
|      * Only defined on the root element! | ||||
|      */ | ||||
|     public readonly loadedTiles: Map<number, TiledFeatureSource & FeatureSourceForLayer> = undefined; | ||||
|     public readonly loadedTiles: Map<number, TiledFeatureSource & FeatureSourceForLayer> = undefined | ||||
| 
 | ||||
|     public readonly maxFeatureCount: number; | ||||
|     public readonly name; | ||||
|     public readonly features: UIEventSource<{ feature: any, freshness: Date }[]> | ||||
|     public readonly maxFeatureCount: number | ||||
|     public readonly name | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> | ||||
|     public readonly containedIds: Store<Set<string>> | ||||
| 
 | ||||
|     public readonly bbox: BBox; | ||||
|     public readonly tileIndex: number; | ||||
|     public readonly bbox: BBox | ||||
|     public readonly tileIndex: number | ||||
|     private upper_left: TiledFeatureSource | ||||
|     private upper_right: TiledFeatureSource | ||||
|     private lower_left: TiledFeatureSource | ||||
|     private lower_right: TiledFeatureSource | ||||
|     private readonly maxzoom: number; | ||||
|     private readonly maxzoom: number | ||||
|     private readonly options: TiledFeatureSourceOptions | ||||
| 
 | ||||
|     private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) { | ||||
|         this.z = z; | ||||
|         this.x = x; | ||||
|         this.y = y; | ||||
|     private constructor( | ||||
|         z: number, | ||||
|         x: number, | ||||
|         y: number, | ||||
|         parent: TiledFeatureSource, | ||||
|         options?: TiledFeatureSourceOptions | ||||
|     ) { | ||||
|         this.z = z | ||||
|         this.x = x | ||||
|         this.y = y | ||||
|         this.bbox = BBox.fromTile(z, x, y) | ||||
|         this.tileIndex = Tiles.tile_index(z, x, y) | ||||
|         this.name = `TiledFeatureSource(${z},${x},${y})` | ||||
|         this.parent = parent; | ||||
|         this.parent = parent | ||||
|         this.layer = options.layer | ||||
|         options = options ?? {} | ||||
|         this.maxFeatureCount = options?.maxFeatureCount ?? 250; | ||||
|         this.maxFeatureCount = options?.maxFeatureCount ?? 250 | ||||
|         this.maxzoom = options.maxZoomLevel ?? 18 | ||||
|         this.options = options; | ||||
|         this.options = options | ||||
|         if (parent === undefined) { | ||||
|             throw "Parent is not allowed to be undefined. Use null instead" | ||||
|         } | ||||
|  | @ -55,50 +67,51 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | |||
|             throw "Invalid root tile: z, x and y should all be null" | ||||
|         } | ||||
|         if (parent === null) { | ||||
|             this.root = this; | ||||
|             this.root = this | ||||
|             this.loadedTiles = new Map() | ||||
|         } else { | ||||
|             this.root = this.parent.root; | ||||
|             this.loadedTiles = this.root.loadedTiles; | ||||
|             this.root = this.parent.root | ||||
|             this.loadedTiles = this.root.loadedTiles | ||||
|             const i = Tiles.tile_index(z, x, y) | ||||
|             this.root.loadedTiles.set(i, this) | ||||
|         } | ||||
|         this.features = new UIEventSource<any[]>([]) | ||||
|         this.containedIds = this.features.map(features => { | ||||
|         this.containedIds = this.features.map((features) => { | ||||
|             if (features === undefined) { | ||||
|                 return undefined; | ||||
|                 return undefined | ||||
|             } | ||||
|             return new Set(features.map(f => f.feature.properties.id)) | ||||
|             return new Set(features.map((f) => f.feature.properties.id)) | ||||
|         }) | ||||
| 
 | ||||
|         // We register this tile, but only when there is some data in it
 | ||||
|         if (this.options.registerTile !== undefined) { | ||||
|             this.features.addCallbackAndRunD(features => { | ||||
|             this.features.addCallbackAndRunD((features) => { | ||||
|                 if (features.length === 0) { | ||||
|                     return; | ||||
|                     return | ||||
|                 } | ||||
|                 this.options.registerTile(this) | ||||
|                 return true; | ||||
|                 return true | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public static createHierarchy(features: FeatureSource, options?: TiledFeatureSourceOptions): TiledFeatureSource { | ||||
|     public static createHierarchy( | ||||
|         features: FeatureSource, | ||||
|         options?: TiledFeatureSourceOptions | ||||
|     ): TiledFeatureSource { | ||||
|         options = { | ||||
|             ...options, | ||||
|             layer: features["layer"] ?? options.layer | ||||
|             layer: features["layer"] ?? options.layer, | ||||
|         } | ||||
|         const root = new TiledFeatureSource(0, 0, 0, null, options) | ||||
|         features.features?.addCallbackAndRunD(feats => root.addFeatures(feats)) | ||||
|         return root; | ||||
|         features.features?.addCallbackAndRunD((feats) => root.addFeatures(feats)) | ||||
|         return root | ||||
|     } | ||||
| 
 | ||||
|     private isSplitNeeded(featureCount: number) { | ||||
|         if (this.upper_left !== undefined) { | ||||
|             // This tile has been split previously, so we keep on splitting
 | ||||
|             return true; | ||||
|             return true | ||||
|         } | ||||
|         if (this.z >= this.maxzoom) { | ||||
|             // We are not allowed to split any further
 | ||||
|  | @ -111,7 +124,6 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | |||
| 
 | ||||
|         // To much features - we split
 | ||||
|         return featureCount > this.maxFeatureCount | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /*** | ||||
|  | @ -120,21 +132,45 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | |||
|      * @param features | ||||
|      * @private | ||||
|      */ | ||||
|     private addFeatures(features: { feature: any, freshness: Date }[]) { | ||||
|     private addFeatures(features: { feature: any; freshness: Date }[]) { | ||||
|         if (features === undefined || features.length === 0) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (!this.isSplitNeeded(features.length)) { | ||||
|             this.features.setData(features) | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (this.upper_left === undefined) { | ||||
|             this.upper_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2, this, this.options) | ||||
|             this.upper_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2, this, this.options) | ||||
|             this.lower_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2 + 1, this, this.options) | ||||
|             this.lower_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2 + 1, this, this.options) | ||||
|             this.upper_left = new TiledFeatureSource( | ||||
|                 this.z + 1, | ||||
|                 this.x * 2, | ||||
|                 this.y * 2, | ||||
|                 this, | ||||
|                 this.options | ||||
|             ) | ||||
|             this.upper_right = new TiledFeatureSource( | ||||
|                 this.z + 1, | ||||
|                 this.x * 2 + 1, | ||||
|                 this.y * 2, | ||||
|                 this, | ||||
|                 this.options | ||||
|             ) | ||||
|             this.lower_left = new TiledFeatureSource( | ||||
|                 this.z + 1, | ||||
|                 this.x * 2, | ||||
|                 this.y * 2 + 1, | ||||
|                 this, | ||||
|                 this.options | ||||
|             ) | ||||
|             this.lower_right = new TiledFeatureSource( | ||||
|                 this.z + 1, | ||||
|                 this.x * 2 + 1, | ||||
|                 this.y * 2 + 1, | ||||
|                 this, | ||||
|                 this.options | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         const ulf = [] | ||||
|  | @ -195,19 +231,18 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | |||
|         this.lower_left.addFeatures(llf) | ||||
|         this.lower_right.addFeatures(lrf) | ||||
|         this.features.setData(overlapsboundary) | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export interface TiledFeatureSourceOptions { | ||||
|     readonly maxFeatureCount?: number, | ||||
|     readonly maxZoomLevel?: number, | ||||
|     readonly minZoomLevel?: number, | ||||
|     readonly maxFeatureCount?: number | ||||
|     readonly maxZoomLevel?: number | ||||
|     readonly minZoomLevel?: number | ||||
|     /** | ||||
|      * IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated. | ||||
|      * Setting 'dontEnforceMinZoomLevel' will assign to feature to some matching subtile. | ||||
|      */ | ||||
|     readonly noDuplicates?: boolean, | ||||
|     readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void, | ||||
|     readonly noDuplicates?: boolean | ||||
|     readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void | ||||
|     readonly layer?: FilteredLayer | ||||
| } | ||||
|  | @ -1,17 +1,25 @@ | |||
| import * as turf from '@turf/turf' | ||||
| import {BBox} from "./BBox"; | ||||
| import * as turf from "@turf/turf" | ||||
| import { BBox } from "./BBox" | ||||
| import togpx from "togpx" | ||||
| import Constants from "../Models/Constants"; | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| import {AllGeoJSON, booleanWithin, Coord, Feature, Geometry, MultiPolygon, Polygon, Properties} from "@turf/turf"; | ||||
| import Constants from "../Models/Constants" | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||
| import { | ||||
|     AllGeoJSON, | ||||
|     booleanWithin, | ||||
|     Coord, | ||||
|     Feature, | ||||
|     Geometry, | ||||
|     MultiPolygon, | ||||
|     Polygon, | ||||
|     Properties, | ||||
| } from "@turf/turf" | ||||
| 
 | ||||
| export class GeoOperations { | ||||
| 
 | ||||
|     private static readonly _earthRadius = 6378137; | ||||
|     private static readonly _originShift = 2 * Math.PI * GeoOperations._earthRadius / 2; | ||||
|     private static readonly _earthRadius = 6378137 | ||||
|     private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2 | ||||
| 
 | ||||
|     static surfaceAreaInSqMeters(feature: any) { | ||||
|         return turf.area(feature); | ||||
|         return turf.area(feature) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -19,10 +27,10 @@ export class GeoOperations { | |||
|      * @param feature | ||||
|      */ | ||||
|     static centerpoint(feature: any) { | ||||
|         const newFeature = turf.center(feature); | ||||
|         newFeature.properties = feature.properties; | ||||
|         newFeature.id = feature.id; | ||||
|         return newFeature; | ||||
|         const newFeature = turf.center(feature) | ||||
|         newFeature.properties = feature.properties | ||||
|         newFeature.id = feature.id | ||||
|         return newFeature | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -30,7 +38,7 @@ export class GeoOperations { | |||
|      * @param feature | ||||
|      */ | ||||
|     static centerpointCoordinates(feature: AllGeoJSON): [number, number] { | ||||
|         return <[number, number]>turf.center(feature).geometry.coordinates; | ||||
|         return <[number, number]>turf.center(feature).geometry.coordinates | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -39,7 +47,7 @@ export class GeoOperations { | |||
|      * @param lonlat1 | ||||
|      */ | ||||
|     static distanceBetween(lonlat0: [number, number], lonlat1: [number, number]) { | ||||
|         return turf.distance(lonlat0, lonlat1, {units: "meters"}) | ||||
|         return turf.distance(lonlat0, lonlat1, { units: "meters" }) | ||||
|     } | ||||
| 
 | ||||
|     static convexHull(featureCollection, options: { concavity?: number }) { | ||||
|  | @ -69,16 +77,17 @@ export class GeoOperations { | |||
|      * const overlap0 = GeoOperations.calculateOverlap(line0, [polygon]); | ||||
|      * overlap.length // => 1
 | ||||
|      */ | ||||
|     static calculateOverlap(feature: any, otherFeatures: any[]): { feat: any, overlap: number }[] { | ||||
| 
 | ||||
|         const featureBBox = BBox.get(feature); | ||||
|         const result: { feat: any, overlap: number }[] = []; | ||||
|     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; | ||||
|             const coor = feature.geometry.coordinates | ||||
|             for (const otherFeature of otherFeatures) { | ||||
| 
 | ||||
|                 if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) { | ||||
|                     continue; | ||||
|                 if ( | ||||
|                     feature.properties.id !== undefined && | ||||
|                     feature.properties.id === otherFeature.properties.id | ||||
|                 ) { | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 if (otherFeature.geometry === undefined) { | ||||
|  | @ -87,76 +96,95 @@ export class GeoOperations { | |||
|                 } | ||||
| 
 | ||||
|                 if (GeoOperations.inside(coor, otherFeature)) { | ||||
|                     result.push({feat: otherFeature, overlap: undefined}) | ||||
|                     result.push({ feat: otherFeature, overlap: undefined }) | ||||
|                 } | ||||
|             } | ||||
|             return result; | ||||
|             return result | ||||
|         } | ||||
| 
 | ||||
|         if (feature.geometry.type === "LineString") { | ||||
| 
 | ||||
|             for (const otherFeature of otherFeatures) { | ||||
| 
 | ||||
|                 if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) { | ||||
|                     continue; | ||||
|                 if ( | ||||
|                     feature.properties.id !== undefined && | ||||
|                     feature.properties.id === otherFeature.properties.id | ||||
|                 ) { | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 const intersection = GeoOperations.calculateInstersection(feature, otherFeature, featureBBox) | ||||
|                 const intersection = GeoOperations.calculateInstersection( | ||||
|                     feature, | ||||
|                     otherFeature, | ||||
|                     featureBBox | ||||
|                 ) | ||||
|                 if (intersection === null) { | ||||
|                     continue | ||||
|                 } | ||||
|                 result.push({feat: otherFeature, overlap: intersection}) | ||||
| 
 | ||||
|                 result.push({ feat: otherFeature, overlap: intersection }) | ||||
|             } | ||||
|             return result; | ||||
|             return result | ||||
|         } | ||||
| 
 | ||||
|         if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") { | ||||
| 
 | ||||
|             for (const otherFeature of otherFeatures) { | ||||
| 
 | ||||
|                 if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) { | ||||
|                     continue; | ||||
|                 if ( | ||||
|                     feature.properties.id !== undefined && | ||||
|                     feature.properties.id === otherFeature.properties.id | ||||
|                 ) { | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 if (otherFeature.geometry.type === "Point") { | ||||
|                     if (this.inside(otherFeature, feature)) { | ||||
|                         result.push({feat: otherFeature, overlap: undefined}) | ||||
|                         result.push({ feat: otherFeature, overlap: undefined }) | ||||
|                     } | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 // Calculate the surface area of the intersection
 | ||||
| 
 | ||||
|                 const intersection = this.calculateInstersection(feature, otherFeature, featureBBox) | ||||
|                 if (intersection === null) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
|                 result.push({feat: otherFeature, overlap: intersection}) | ||||
| 
 | ||||
|                 result.push({ feat: otherFeature, overlap: intersection }) | ||||
|             } | ||||
|             return result; | ||||
|             return result | ||||
|         } | ||||
|         console.error("Could not correctly calculate the overlap of ", feature, ": unsupported type") | ||||
|         return result; | ||||
|         console.error( | ||||
|             "Could not correctly calculate the overlap of ", | ||||
|             feature, | ||||
|             ": unsupported type" | ||||
|         ) | ||||
|         return result | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Helper function which does the heavy lifting for 'inside' | ||||
|      */ | ||||
|     private static pointInPolygonCoordinates(x: number, y: number, coordinates: [number, number][][]) { | ||||
|         const inside = GeoOperations.pointWithinRing(x, y, /*This is the outer ring of the polygon */coordinates[0]) | ||||
|     private static pointInPolygonCoordinates( | ||||
|         x: number, | ||||
|         y: number, | ||||
|         coordinates: [number, number][][] | ||||
|     ) { | ||||
|         const inside = GeoOperations.pointWithinRing( | ||||
|             x, | ||||
|             y, | ||||
|             /*This is the outer ring of the polygon */ coordinates[0] | ||||
|         ) | ||||
|         if (!inside) { | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
|         for (let i = 1; i < coordinates.length; i++) { | ||||
|             const inHole = GeoOperations.pointWithinRing(x, y, coordinates[i] /* These are inner rings, aka holes*/) | ||||
|             const inHole = GeoOperations.pointWithinRing( | ||||
|                 x, | ||||
|                 y, | ||||
|                 coordinates[i] /* These are inner rings, aka holes*/ | ||||
|             ) | ||||
|             if (inHole) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
|         return true; | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -186,37 +214,32 @@ export class GeoOperations { | |||
|         // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
 | ||||
| 
 | ||||
|         if (feature.geometry.type === "Point") { | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
| 
 | ||||
|         if (pointCoordinate.geometry !== undefined) { | ||||
|             pointCoordinate = pointCoordinate.geometry.coordinates | ||||
|         } | ||||
| 
 | ||||
|         const x: number = pointCoordinate[0]; | ||||
|         const y: number = pointCoordinate[1]; | ||||
| 
 | ||||
|         const x: number = pointCoordinate[0] | ||||
|         const y: number = pointCoordinate[1] | ||||
| 
 | ||||
|         if (feature.geometry.type === "MultiPolygon") { | ||||
|             const coordinatess = feature.geometry.coordinates; | ||||
|             const coordinatess = feature.geometry.coordinates | ||||
|             for (const coordinates of coordinatess) { | ||||
|                 const inThisPolygon = GeoOperations.pointInPolygonCoordinates(x, y, coordinates) | ||||
|                 if (inThisPolygon) { | ||||
|                     return true; | ||||
|                     return true | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         if (feature.geometry.type === "Polygon") { | ||||
|             return GeoOperations.pointInPolygonCoordinates(x, y, feature.geometry.coordinates) | ||||
|         } | ||||
| 
 | ||||
|         throw "GeoOperations.inside: unsupported geometry type "+feature.geometry.type | ||||
| 
 | ||||
| 
 | ||||
|         throw "GeoOperations.inside: unsupported geometry type " + feature.geometry.type | ||||
|     } | ||||
| 
 | ||||
|     static lengthInMeters(feature: any) { | ||||
|  | @ -225,39 +248,24 @@ export class GeoOperations { | |||
| 
 | ||||
|     static buffer(feature: any, bufferSizeInMeter: number) { | ||||
|         return turf.buffer(feature, bufferSizeInMeter / 1000, { | ||||
|             units: 'kilometers' | ||||
|             units: "kilometers", | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     static bbox(feature: any) { | ||||
|         const [lon, lat, lon0, lat0] = turf.bbox(feature) | ||||
|         return { | ||||
|             "type": "Feature", | ||||
|             "geometry": { | ||||
|                 "type": "LineString", | ||||
|                 "coordinates": [ | ||||
|                     [ | ||||
|                         lon, | ||||
|                         lat | ||||
|             type: "Feature", | ||||
|             geometry: { | ||||
|                 type: "LineString", | ||||
|                 coordinates: [ | ||||
|                     [lon, lat], | ||||
|                     [lon0, lat], | ||||
|                     [lon0, lat0], | ||||
|                     [lon, lat0], | ||||
|                     [lon, lat], | ||||
|                 ], | ||||
|                     [ | ||||
|                         lon0, | ||||
|                         lat | ||||
|                     ], | ||||
|                     [ | ||||
|                         lon0, | ||||
|                         lat0 | ||||
|                     ], | ||||
|                     [ | ||||
|                         lon, | ||||
|                         lat0 | ||||
|                     ], | ||||
|                     [ | ||||
|                         lon, | ||||
|                         lat | ||||
|                     ], | ||||
|                 ] | ||||
|             } | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -273,18 +281,17 @@ export class GeoOperations { | |||
|      */ | ||||
|     public static nearestPoint(way, point: [number, number]) { | ||||
|         if (way.geometry.type === "Polygon") { | ||||
|             way = {...way} | ||||
|             way.geometry = {...way.geometry} | ||||
|             way = { ...way } | ||||
|             way.geometry = { ...way.geometry } | ||||
|             way.geometry.type = "LineString" | ||||
|             way.geometry.coordinates = way.geometry.coordinates[0] | ||||
|         } | ||||
| 
 | ||||
|         return turf.nearestPointOnLine(way, point, {units: "kilometers"}); | ||||
|         return turf.nearestPointOnLine(way, point, { units: "kilometers" }) | ||||
|     } | ||||
| 
 | ||||
|     public static toCSV(features: any[]): string { | ||||
| 
 | ||||
|         const headerValuesSeen = new Set<string>(); | ||||
|         const headerValuesSeen = new Set<string>() | ||||
|         const headerValuesOrdered: string[] = [] | ||||
| 
 | ||||
|         function addH(key) { | ||||
|  | @ -300,18 +307,17 @@ export class GeoOperations { | |||
|         const lines: string[] = [] | ||||
| 
 | ||||
|         for (const feature of features) { | ||||
|             const properties = feature.properties; | ||||
|             const properties = feature.properties | ||||
|             for (const key in properties) { | ||||
|                 if (!properties.hasOwnProperty(key)) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
|                 addH(key) | ||||
| 
 | ||||
|             } | ||||
|         } | ||||
|         headerValuesOrdered.sort() | ||||
|         for (const feature of features) { | ||||
|             const properties = feature.properties; | ||||
|             const properties = feature.properties | ||||
|             let line = "" | ||||
|             for (const key of headerValuesOrdered) { | ||||
|                 const value = properties[key] | ||||
|  | @ -324,27 +330,27 @@ export class GeoOperations { | |||
|             lines.push(line) | ||||
|         } | ||||
| 
 | ||||
|         return headerValuesOrdered.map(v => JSON.stringify(v)).join(",") + "\n" + lines.join("\n") | ||||
|         return headerValuesOrdered.map((v) => JSON.stringify(v)).join(",") + "\n" + lines.join("\n") | ||||
|     } | ||||
| 
 | ||||
|     //Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913
 | ||||
|     public static ConvertWgs84To900913(lonLat: [number, number]): [number, number] { | ||||
|         const lon = lonLat[0]; | ||||
|         const lat = lonLat[1]; | ||||
|         const x = lon * GeoOperations._originShift / 180; | ||||
|         let y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); | ||||
|         y = y * GeoOperations._originShift / 180; | ||||
|         return [x, y]; | ||||
|         const lon = lonLat[0] | ||||
|         const lat = lonLat[1] | ||||
|         const x = (lon * GeoOperations._originShift) / 180 | ||||
|         let y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360)) / (Math.PI / 180) | ||||
|         y = (y * GeoOperations._originShift) / 180 | ||||
|         return [x, y] | ||||
|     } | ||||
| 
 | ||||
|     //Converts XY point from (Spherical) Web Mercator EPSG:3785 (unofficially EPSG:900913) to lat/lon in WGS84 Datum
 | ||||
|     public static Convert900913ToWgs84(lonLat: [number, number]): [number, number] { | ||||
|         const lon = lonLat[0] | ||||
|         const lat = lonLat[1] | ||||
|         const x = 180 * lon / GeoOperations._originShift; | ||||
|         let y = 180 * lat / GeoOperations._originShift; | ||||
|         y = 180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2); | ||||
|         return [x, y]; | ||||
|         const x = (180 * lon) / GeoOperations._originShift | ||||
|         let y = (180 * lat) / GeoOperations._originShift | ||||
|         y = (180 / Math.PI) * (2 * Math.atan(Math.exp((y * Math.PI) / 180)) - Math.PI / 2) | ||||
|         return [x, y] | ||||
|     } | ||||
| 
 | ||||
|     public static GeoJsonToWGS84(geojson) { | ||||
|  | @ -360,10 +366,10 @@ export class GeoOperations { | |||
|     public static SimplifyCoordinates(coordinates: [number, number][]) { | ||||
|         const newCoordinates = [] | ||||
|         for (let i = 1; i < coordinates.length - 1; i++) { | ||||
|             const coordinate = coordinates[i]; | ||||
|             const coordinate = coordinates[i] | ||||
|             const prev = coordinates[i - 1] | ||||
|             const next = coordinates[i + 1] | ||||
|             const b0 = turf.bearing(prev, coordinate, {final: true}) | ||||
|             const b0 = turf.bearing(prev, coordinate, { final: true }) | ||||
|             const b1 = turf.bearing(coordinate, next) | ||||
| 
 | ||||
|             const diff = Math.abs(b1 - b0) | ||||
|  | @ -373,27 +379,27 @@ export class GeoOperations { | |||
|             newCoordinates.push(coordinate) | ||||
|         } | ||||
|         return newCoordinates | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Calculates line intersection between two features. | ||||
|      */ | ||||
|     public static LineIntersections(feature, otherFeature): [number, number][] { | ||||
|         return turf.lineIntersect(feature, otherFeature).features.map(p => <[number, number]>p.geometry.coordinates) | ||||
|         return turf | ||||
|             .lineIntersect(feature, otherFeature) | ||||
|             .features.map((p) => <[number, number]>p.geometry.coordinates) | ||||
|     } | ||||
| 
 | ||||
|     public static AsGpx(feature, generatedWithLayer?: LayerConfig) { | ||||
| 
 | ||||
|         const metadata = {} | ||||
|         const tags = feature.properties | ||||
| 
 | ||||
|         if (generatedWithLayer !== undefined) { | ||||
| 
 | ||||
|             metadata["name"] = generatedWithLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt | ||||
|             metadata["desc"] = "Generated with MapComplete layer " + generatedWithLayer.id | ||||
|             if (tags._backend?.contains("openstreetmap")) { | ||||
|                 metadata["copyright"] = "Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright" | ||||
|                 metadata["copyright"] = | ||||
|                     "Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright" | ||||
|                 metadata["author"] = tags["_last_edit:contributor"] | ||||
|                 metadata["link"] = "https://www.openstreetmap.org/" + tags.id | ||||
|                 metadata["time"] = tags["_last_edit:timestamp"] | ||||
|  | @ -404,18 +410,22 @@ export class GeoOperations { | |||
| 
 | ||||
|         return togpx(feature, { | ||||
|             creator: "MapComplete " + Constants.vNumber, | ||||
|             metadata | ||||
|             metadata, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public static IdentifieCommonSegments(coordinatess: [number, number][][]): { | ||||
|         originalIndex: number, | ||||
|         segmentShardWith: number[], | ||||
|         originalIndex: number | ||||
|         segmentShardWith: number[] | ||||
|         coordinates: [] | ||||
|     }[] { | ||||
| 
 | ||||
|         // An edge. Note that the edge might be reversed to fix the sorting condition:  start[0] < end[0] && (start[0] != end[0] || start[0] < end[1])
 | ||||
|         type edge = { start: [number, number], end: [number, number], intermediate: [number, number][], members: { index: number, isReversed: boolean }[] } | ||||
|         type edge = { | ||||
|             start: [number, number] | ||||
|             end: [number, number] | ||||
|             intermediate: [number, number][] | ||||
|             members: { index: number; isReversed: boolean }[] | ||||
|         } | ||||
| 
 | ||||
|         // The strategy:
 | ||||
|         // 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them
 | ||||
|  | @ -425,12 +435,11 @@ export class GeoOperations { | |||
|         const allEdgesByKey = new Map<string, edge>() | ||||
| 
 | ||||
|         for (let index = 0; index < coordinatess.length; index++) { | ||||
|             const coordinates = coordinatess[index]; | ||||
|             const coordinates = coordinatess[index] | ||||
|             for (let i = 0; i < coordinates.length - 1; i++) { | ||||
| 
 | ||||
|                 const c0 = coordinates[i]; | ||||
|                 const c0 = coordinates[i] | ||||
|                 const c1 = coordinates[i + 1] | ||||
|                 const isReversed = (c0[0] > c1[0]) || (c0[0] == c1[0] && c0[1] > c1[1]) | ||||
|                 const isReversed = c0[0] > c1[0] || (c0[0] == c1[0] && c0[1] > c1[1]) | ||||
| 
 | ||||
|                 let key: string | ||||
|                 if (isReversed) { | ||||
|  | @ -438,40 +447,38 @@ export class GeoOperations { | |||
|                 } else { | ||||
|                     key = "" + c0 + ";" + c1 | ||||
|                 } | ||||
|                 const member = {index, isReversed} | ||||
|                 const member = { index, isReversed } | ||||
|                 if (allEdgesByKey.has(key)) { | ||||
|                     allEdgesByKey.get(key).members.push(member) | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 let edge: edge; | ||||
|                 let edge: edge | ||||
|                 if (!isReversed) { | ||||
|                     edge = { | ||||
|                         start: c0, | ||||
|                         end: c1, | ||||
|                         members: [member], | ||||
|                         intermediate: [] | ||||
|                         intermediate: [], | ||||
|                     } | ||||
|                 } else { | ||||
|                     edge = { | ||||
|                         start: c1, | ||||
|                         end: c0, | ||||
|                         members: [member], | ||||
|                         intermediate: [] | ||||
|                         intermediate: [], | ||||
|                     } | ||||
|                 } | ||||
|                 allEdgesByKey.set(key, edge) | ||||
| 
 | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Lets merge them back together!
 | ||||
| 
 | ||||
|         let didMergeSomething = false; | ||||
|         let didMergeSomething = false | ||||
|         let allMergedEdges = Array.from(allEdgesByKey.values()) | ||||
|         const allEdgesByStartPoint = new Map<string, edge[]>() | ||||
|         for (const edge of allMergedEdges) { | ||||
| 
 | ||||
|             edge.members.sort((m0, m1) => m0.index - m1.index) | ||||
| 
 | ||||
|             const kstart = edge.start + "" | ||||
|  | @ -481,7 +488,6 @@ export class GeoOperations { | |||
|             allEdgesByStartPoint.get(kstart).push(edge) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         function membersAreCompatible(first: edge, second: edge): boolean { | ||||
|             // There must be an exact match between the members
 | ||||
|             if (first.members === second.members) { | ||||
|  | @ -504,7 +510,6 @@ export class GeoOperations { | |||
|             // Allrigth, they are the same, lets mark this permanently
 | ||||
|             second.members = first.members | ||||
|             return true | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         do { | ||||
|  | @ -524,9 +529,8 @@ export class GeoOperations { | |||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 for (let i = 0; i < matchingEndEdges.length; i++) { | ||||
|                     const endEdge = matchingEndEdges[i]; | ||||
|                     const endEdge = matchingEndEdges[i] | ||||
| 
 | ||||
|                     if (consumed.has(endEdge)) { | ||||
|                         continue | ||||
|  | @ -543,12 +547,11 @@ export class GeoOperations { | |||
|                     edge.end = endEdge.end | ||||
|                     consumed.add(endEdge) | ||||
|                     matchingEndEdges.splice(i, 1) | ||||
|                     break; | ||||
|                     break | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             allMergedEdges = allMergedEdges.filter(edge => !consumed.has(edge)); | ||||
| 
 | ||||
|             allMergedEdges = allMergedEdges.filter((edge) => !consumed.has(edge)) | ||||
|         } while (didMergeSomething) | ||||
| 
 | ||||
|         return [] | ||||
|  | @ -569,7 +572,7 @@ export class GeoOperations { | |||
| 
 | ||||
|         const copy = { | ||||
|             ...feature, | ||||
|             geometry: {...feature.geometry} | ||||
|             geometry: { ...feature.geometry }, | ||||
|         } | ||||
|         let coordinates: [number, number][] | ||||
|         if (feature.geometry.type === "LineString") { | ||||
|  | @ -582,7 +585,7 @@ export class GeoOperations { | |||
| 
 | ||||
|         // inline replacement in the coordinates list
 | ||||
|         for (let i = coordinates.length - 2; i >= 1; i--) { | ||||
|             const coordinate = coordinates[i]; | ||||
|             const coordinate = coordinates[i] | ||||
|             const nextCoordinate = coordinates[i + 1] | ||||
|             const prevCoordinate = coordinates[i - 1] | ||||
| 
 | ||||
|  | @ -610,30 +613,27 @@ export class GeoOperations { | |||
|                 // In case that the line is going south, e.g. bearingN = 179, bearingP = -179
 | ||||
|                 coordinates.splice(i, 1) | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
|         return copy; | ||||
| 
 | ||||
|         return copy | ||||
|     } | ||||
| 
 | ||||
|     private static pointWithinRing(x: number, y: number, ring: [number, number][]) { | ||||
|         let inside = false; | ||||
|         let inside = false | ||||
|         for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { | ||||
|             const coori = ring[i]; | ||||
|             const coorj = ring[j]; | ||||
|             const coori = ring[i] | ||||
|             const coorj = ring[j] | ||||
| 
 | ||||
|             const xi = coori[0]; | ||||
|             const yi = coori[1]; | ||||
|             const xj = coorj[0]; | ||||
|             const yj = coorj[1]; | ||||
|             const xi = coori[0] | ||||
|             const yi = coori[1] | ||||
|             const xj = coorj[0] | ||||
|             const yj = coorj[1] | ||||
| 
 | ||||
|             const intersect = ((yi > y) != (yj > y)) | ||||
|                 && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); | ||||
|             const intersect = yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi | ||||
|             if (intersect) { | ||||
|                 inside = !inside; | ||||
|                 inside = !inside | ||||
|             } | ||||
|         } | ||||
|         return inside; | ||||
|         return inside | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -642,46 +642,47 @@ export class GeoOperations { | |||
|      * Returns 0 if both are linestrings | ||||
|      * Returns null if the features are not intersecting | ||||
|      */ | ||||
|     private static calculateInstersection(feature, otherFeature, featureBBox: BBox, otherFeatureBBox?: BBox): number { | ||||
|     private static calculateInstersection( | ||||
|         feature, | ||||
|         otherFeature, | ||||
|         featureBBox: BBox, | ||||
|         otherFeatureBBox?: BBox | ||||
|     ): number { | ||||
|         if (feature.geometry.type === "LineString") { | ||||
| 
 | ||||
| 
 | ||||
|             otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature); | ||||
|             otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature) | ||||
|             const overlaps = featureBBox.overlapsWith(otherFeatureBBox) | ||||
|             if (!overlaps) { | ||||
|                 return null; | ||||
|                 return null | ||||
|             } | ||||
| 
 | ||||
|             // Calculate the length of the intersection
 | ||||
| 
 | ||||
| 
 | ||||
|             let intersectionPoints = turf.lineIntersect(feature, otherFeature); | ||||
|             let intersectionPoints = turf.lineIntersect(feature, otherFeature) | ||||
|             if (intersectionPoints.features.length == 0) { | ||||
|                 // No intersections.
 | ||||
|                 // If one point is inside of the polygon, all points are
 | ||||
| 
 | ||||
| 
 | ||||
|                 const coors = feature.geometry.coordinates; | ||||
|                 const coors = feature.geometry.coordinates | ||||
|                 const startCoor = coors[0] | ||||
|                 if (this.inside(startCoor, otherFeature)) { | ||||
|                     return this.lengthInMeters(feature) | ||||
|                 } | ||||
| 
 | ||||
|                 return null; | ||||
|                 return null | ||||
|             } | ||||
|             let intersectionPointsArray = intersectionPoints.features.map(d => { | ||||
|             let intersectionPointsArray = intersectionPoints.features.map((d) => { | ||||
|                 return d.geometry.coordinates | ||||
|             }); | ||||
|             }) | ||||
| 
 | ||||
|             if (otherFeature.geometry.type === "LineString") { | ||||
|                 if (intersectionPointsArray.length > 0) { | ||||
|                     return 0 | ||||
|                 } | ||||
|                 return null; | ||||
|                 return null | ||||
|             } | ||||
|             if (intersectionPointsArray.length == 1) { | ||||
|                 // We need to add the start- or endpoint of the current feature, depending on which one is embedded
 | ||||
|                 const coors = feature.geometry.coordinates; | ||||
|                 const coors = feature.geometry.coordinates | ||||
|                 const startCoor = coors[0] | ||||
|                 if (this.inside(startCoor, otherFeature)) { | ||||
|                     // The startpoint is embedded
 | ||||
|  | @ -691,46 +692,50 @@ export class GeoOperations { | |||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             let intersection = turf.lineSlice(turf.point(intersectionPointsArray[0]), turf.point(intersectionPointsArray[1]), feature); | ||||
|             let intersection = turf.lineSlice( | ||||
|                 turf.point(intersectionPointsArray[0]), | ||||
|                 turf.point(intersectionPointsArray[1]), | ||||
|                 feature | ||||
|             ) | ||||
| 
 | ||||
|             if (intersection == null) { | ||||
|                 return null; | ||||
|                 return null | ||||
|             } | ||||
|             const intersectionSize = turf.length(intersection); // in km
 | ||||
|             const intersectionSize = turf.length(intersection) // in km
 | ||||
|             return intersectionSize * 1000 | ||||
| 
 | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") { | ||||
|             const otherFeatureBBox = BBox.get(otherFeature); | ||||
|             const otherFeatureBBox = BBox.get(otherFeature) | ||||
|             const overlaps = featureBBox.overlapsWith(otherFeatureBBox) | ||||
|             if (!overlaps) { | ||||
|                 return null; | ||||
|                 return null | ||||
|             } | ||||
|             if (otherFeature.geometry.type === "LineString") { | ||||
|                 return this.calculateInstersection(otherFeature, feature, otherFeatureBBox, featureBBox) | ||||
|                 return this.calculateInstersection( | ||||
|                     otherFeature, | ||||
|                     feature, | ||||
|                     otherFeatureBBox, | ||||
|                     featureBBox | ||||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
| 
 | ||||
|                 const intersection = turf.intersect(feature, otherFeature); | ||||
|                 const intersection = turf.intersect(feature, otherFeature) | ||||
|                 if (intersection == null) { | ||||
|                     return null; | ||||
|                     return null | ||||
|                 } | ||||
|                 return turf.area(intersection); // in m²
 | ||||
|                 return turf.area(intersection) // in m²
 | ||||
|             } catch (e) { | ||||
|                 if (e.message === "Each LinearRing of a Polygon must have 4 or more Positions.") { | ||||
|                     // WORKAROUND TIME!
 | ||||
|                     // See https://github.com/Turfjs/turf/pull/2238
 | ||||
|                     return null; | ||||
|                     return null | ||||
|                 } | ||||
|                 throw e; | ||||
|                 throw e | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
|         throw "CalculateIntersection fallthrough: can not calculate an intersection between features" | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -769,9 +774,10 @@ export class GeoOperations { | |||
|      * GeoOperations.completelyWithin(pond, park) // => true
 | ||||
|      * GeoOperations.completelyWithin(park, pond) // => false
 | ||||
|      */ | ||||
|     static completelyWithin(feature: Feature<Geometry, any>, possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any>) : boolean { | ||||
|         return booleanWithin(feature, possiblyEncloingFeature); | ||||
|     static completelyWithin( | ||||
|         feature: Feature<Geometry, any>, | ||||
|         possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any> | ||||
|     ): boolean { | ||||
|         return booleanWithin(feature, possiblyEncloingFeature) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,45 +1,52 @@ | |||
| import {Mapillary} from "./Mapillary"; | ||||
| import {WikimediaImageProvider} from "./WikimediaImageProvider"; | ||||
| import {Imgur} from "./Imgur"; | ||||
| import GenericImageProvider from "./GenericImageProvider"; | ||||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import ImageProvider, {ProvidedImage} from "./ImageProvider"; | ||||
| import {WikidataImageProvider} from "./WikidataImageProvider"; | ||||
| import { Mapillary } from "./Mapillary" | ||||
| import { WikimediaImageProvider } from "./WikimediaImageProvider" | ||||
| import { Imgur } from "./Imgur" | ||||
| import GenericImageProvider from "./GenericImageProvider" | ||||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||
| import { WikidataImageProvider } from "./WikidataImageProvider" | ||||
| 
 | ||||
| /** | ||||
|  * A generic 'from the interwebz' image picker, without attribution | ||||
|  */ | ||||
| export default class AllImageProviders { | ||||
| 
 | ||||
|     public static ImageAttributionSource: ImageProvider[] = [ | ||||
|         Imgur.singleton, | ||||
|         Mapillary.singleton, | ||||
|         WikidataImageProvider.singleton, | ||||
|         WikimediaImageProvider.singleton, | ||||
|         new GenericImageProvider( | ||||
|             [].concat(...Imgur.defaultValuePrefix, ...WikimediaImageProvider.commonsPrefixes, ...Mapillary.valuePrefixes) | ||||
|             [].concat( | ||||
|                 ...Imgur.defaultValuePrefix, | ||||
|                 ...WikimediaImageProvider.commonsPrefixes, | ||||
|                 ...Mapillary.valuePrefixes | ||||
|             ) | ||||
|         ), | ||||
|     ] | ||||
| 
 | ||||
|     private static providersByName= { | ||||
|         "imgur": Imgur.singleton, | ||||
| "mapillary":        Mapillary.singleton, | ||||
|      "wikidata":  WikidataImageProvider.singleton, | ||||
|        "wikimedia": WikimediaImageProvider.singleton | ||||
|     private static providersByName = { | ||||
|         imgur: Imgur.singleton, | ||||
|         mapillary: Mapillary.singleton, | ||||
|         wikidata: WikidataImageProvider.singleton, | ||||
|         wikimedia: WikimediaImageProvider.singleton, | ||||
|     } | ||||
| 
 | ||||
|     public static byName(name: string){ | ||||
|     public static byName(name: string) { | ||||
|         return AllImageProviders.providersByName[name.toLowerCase()] | ||||
|     } | ||||
| 
 | ||||
|     public static defaultKeys = [].concat(AllImageProviders.ImageAttributionSource.map(provider => provider.defaultKeyPrefixes)) | ||||
|     public static defaultKeys = [].concat( | ||||
|         AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes) | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
|     private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<string, UIEventSource<ProvidedImage[]>>() | ||||
|     private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map< | ||||
|         string, | ||||
|         UIEventSource<ProvidedImage[]> | ||||
|     >() | ||||
| 
 | ||||
|     public static LoadImagesFor(tags: Store<any>, tagKey?: string[]): Store<ProvidedImage[]> { | ||||
|         if (tags.data.id === undefined) { | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         const cacheKey = tags.data.id + tagKey | ||||
|  | @ -48,23 +55,21 @@ export default class AllImageProviders { | |||
|             return cached | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const source = new UIEventSource([]) | ||||
|         this._cache.set(cacheKey, source) | ||||
|         const allSources = [] | ||||
|         for (const imageProvider of AllImageProviders.ImageAttributionSource) { | ||||
| 
 | ||||
|             let prefixes = imageProvider.defaultKeyPrefixes | ||||
|             if (tagKey !== undefined) { | ||||
|                 prefixes = tagKey | ||||
|             } | ||||
| 
 | ||||
|             const singleSource = imageProvider.GetRelevantUrls(tags, { | ||||
|                 prefixes: prefixes | ||||
|                 prefixes: prefixes, | ||||
|             }) | ||||
|             allSources.push(singleSource) | ||||
|             singleSource.addCallbackAndRunD(_ => { | ||||
|                 const all: ProvidedImage[] = [].concat(...allSources.map(source => source.data)) | ||||
|             singleSource.addCallbackAndRunD((_) => { | ||||
|                 const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data)) | ||||
|                 const uniq = [] | ||||
|                 const seen = new Set<string>() | ||||
|                 for (const img of all) { | ||||
|  | @ -77,7 +82,6 @@ export default class AllImageProviders { | |||
|                 source.setData(uniq) | ||||
|             }) | ||||
|         } | ||||
|         return source; | ||||
|         return source | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,18 +1,17 @@ | |||
| import ImageProvider, {ProvidedImage} from "./ImageProvider"; | ||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||
| 
 | ||||
| export default class GenericImageProvider extends ImageProvider { | ||||
|     public defaultKeyPrefixes: string[] = ["image"]; | ||||
|     public defaultKeyPrefixes: string[] = ["image"] | ||||
| 
 | ||||
|     private readonly _valuePrefixBlacklist: string[]; | ||||
|     private readonly _valuePrefixBlacklist: string[] | ||||
| 
 | ||||
|     public constructor(valuePrefixBlacklist: string[]) { | ||||
|         super(); | ||||
|         this._valuePrefixBlacklist = valuePrefixBlacklist; | ||||
|         super() | ||||
|         this._valuePrefixBlacklist = valuePrefixBlacklist | ||||
|     } | ||||
| 
 | ||||
|     async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { | ||||
| 
 | ||||
|         if (this._valuePrefixBlacklist.some(prefix => value.startsWith(prefix))) { | ||||
|         if (this._valuePrefixBlacklist.some((prefix) => value.startsWith(prefix))) { | ||||
|             return [] | ||||
|         } | ||||
| 
 | ||||
|  | @ -23,20 +22,20 @@ export default class GenericImageProvider extends ImageProvider { | |||
|             return [] | ||||
|         } | ||||
| 
 | ||||
|         return [Promise.resolve({ | ||||
|         return [ | ||||
|             Promise.resolve({ | ||||
|                 key: key, | ||||
|                 url: value, | ||||
|             provider: this | ||||
|         })] | ||||
|                 provider: this, | ||||
|             }), | ||||
|         ] | ||||
|     } | ||||
| 
 | ||||
|     SourceIcon(backlinkSource?: string) { | ||||
|         return undefined; | ||||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|     public DownloadAttribution(url: string) { | ||||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,50 +1,53 @@ | |||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import {LicenseInfo} from "./LicenseInfo"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import BaseUIElement from "../../UI/BaseUIElement" | ||||
| import { LicenseInfo } from "./LicenseInfo" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| export interface ProvidedImage { | ||||
|     url: string, | ||||
|     key: string, | ||||
|     url: string | ||||
|     key: string | ||||
|     provider: ImageProvider | ||||
| } | ||||
| 
 | ||||
| export default abstract class ImageProvider { | ||||
| 
 | ||||
|     public abstract readonly defaultKeyPrefixes: string[] | ||||
| 
 | ||||
|     public abstract SourceIcon(backlinkSource?: string): BaseUIElement; | ||||
|     public abstract SourceIcon(backlinkSource?: string): BaseUIElement | ||||
| 
 | ||||
|     /** | ||||
|      * Given a properies object, maps it onto _all_ the available pictures for this imageProvider | ||||
|      */ | ||||
|     public GetRelevantUrls(allTags: Store<any>, options?: { | ||||
|     public GetRelevantUrls( | ||||
|         allTags: Store<any>, | ||||
|         options?: { | ||||
|             prefixes?: string[] | ||||
|     }): UIEventSource<ProvidedImage[]> { | ||||
|         } | ||||
|     ): UIEventSource<ProvidedImage[]> { | ||||
|         const prefixes = options?.prefixes ?? this.defaultKeyPrefixes | ||||
|         if (prefixes === undefined) { | ||||
|             throw "No `defaultKeyPrefixes` defined by this image provider" | ||||
|         } | ||||
|         const relevantUrls = new UIEventSource<{ url: string; key: string; provider: ImageProvider }[]>([]) | ||||
|         const relevantUrls = new UIEventSource< | ||||
|             { url: string; key: string; provider: ImageProvider }[] | ||||
|         >([]) | ||||
|         const seenValues = new Set<string>() | ||||
|         allTags.addCallbackAndRunD(tags => { | ||||
|         allTags.addCallbackAndRunD((tags) => { | ||||
|             for (const key in tags) { | ||||
|                 if (!prefixes.some(prefix => key.startsWith(prefix))) { | ||||
|                 if (!prefixes.some((prefix) => key.startsWith(prefix))) { | ||||
|                     continue | ||||
|                 } | ||||
|                 const values = Utils.NoEmpty(tags[key]?.split(";")?.map(v => v.trim()) ?? []) | ||||
|                 const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? []) | ||||
|                 for (const value of values) { | ||||
| 
 | ||||
|                     if (seenValues.has(value)) { | ||||
|                         continue | ||||
|                     } | ||||
|                     seenValues.add(value) | ||||
|                     this.ExtractUrls(key, value).then(promises => { | ||||
|                     this.ExtractUrls(key, value).then((promises) => { | ||||
|                         for (const promise of promises ?? []) { | ||||
|                             if (promise === undefined) { | ||||
|                                 continue | ||||
|                             } | ||||
|                             promise.then(providedImage => { | ||||
|                             promise.then((providedImage) => { | ||||
|                                 if (providedImage === undefined) { | ||||
|                                     return | ||||
|                                 } | ||||
|  | @ -54,15 +57,12 @@ export default abstract class ImageProvider { | |||
|                         } | ||||
|                     }) | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|             } | ||||
|         }) | ||||
|         return relevantUrls | ||||
|     } | ||||
| 
 | ||||
|     public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>; | ||||
| 
 | ||||
|     public abstract DownloadAttribution(url: string): Promise<LicenseInfo>; | ||||
|     public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> | ||||
| 
 | ||||
|     public abstract DownloadAttribution(url: string): Promise<LicenseInfo> | ||||
| } | ||||
|  | @ -1,93 +1,105 @@ | |||
| import ImageProvider, { ProvidedImage } from "./ImageProvider"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import {LicenseInfo} from "./LicenseInfo"; | ||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||
| import BaseUIElement from "../../UI/BaseUIElement" | ||||
| import { Utils } from "../../Utils" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { LicenseInfo } from "./LicenseInfo" | ||||
| 
 | ||||
| export class Imgur extends ImageProvider { | ||||
| 
 | ||||
|     public static readonly defaultValuePrefix = ["https://i.imgur.com"] | ||||
|     public static readonly singleton = new Imgur(); | ||||
|     public readonly defaultKeyPrefixes: string[] = ["image"]; | ||||
|     public static readonly singleton = new Imgur() | ||||
|     public readonly defaultKeyPrefixes: string[] = ["image"] | ||||
| 
 | ||||
|     private constructor() { | ||||
|         super(); | ||||
|         super() | ||||
|     } | ||||
| 
 | ||||
|     static uploadMultiple( | ||||
|         title: string, description: string, blobs: FileList, | ||||
|         handleSuccessfullUpload: ((imageURL: string) => Promise<void>), | ||||
|         allDone: (() => void), | ||||
|         onFail: ((reason: string) => void), | ||||
|         offset: number = 0) { | ||||
| 
 | ||||
|         title: string, | ||||
|         description: string, | ||||
|         blobs: FileList, | ||||
|         handleSuccessfullUpload: (imageURL: string) => Promise<void>, | ||||
|         allDone: () => void, | ||||
|         onFail: (reason: string) => void, | ||||
|         offset: number = 0 | ||||
|     ) { | ||||
|         if (blobs.length == offset) { | ||||
|             allDone(); | ||||
|             return; | ||||
|             allDone() | ||||
|             return | ||||
|         } | ||||
|         const blob = blobs.item(offset); | ||||
|         const self = this; | ||||
|         this.uploadImage(title, description, blob, | ||||
|         const blob = blobs.item(offset) | ||||
|         const self = this | ||||
|         this.uploadImage( | ||||
|             title, | ||||
|             description, | ||||
|             blob, | ||||
|             async (imageUrl) => { | ||||
|                 await handleSuccessfullUpload(imageUrl); | ||||
|                 await handleSuccessfullUpload(imageUrl) | ||||
|                 self.uploadMultiple( | ||||
|                     title, description, blobs, | ||||
|                     title, | ||||
|                     description, | ||||
|                     blobs, | ||||
|                     handleSuccessfullUpload, | ||||
|                     allDone, | ||||
|                     onFail, | ||||
|                     offset + 1); | ||||
|                     offset + 1 | ||||
|                 ) | ||||
|             }, | ||||
|             onFail | ||||
|         ); | ||||
| 
 | ||||
| 
 | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     static uploadImage(title: string, description: string, blob: File, | ||||
|                        handleSuccessfullUpload: ((imageURL: string) => Promise<void>), | ||||
|                        onFail: (reason: string) => void) { | ||||
|     static uploadImage( | ||||
|         title: string, | ||||
|         description: string, | ||||
|         blob: File, | ||||
|         handleSuccessfullUpload: (imageURL: string) => Promise<void>, | ||||
|         onFail: (reason: string) => void | ||||
|     ) { | ||||
|         const apiUrl = "https://api.imgur.com/3/image" | ||||
|         const apiKey = Constants.ImgurApiKey | ||||
| 
 | ||||
|         const apiUrl = 'https://api.imgur.com/3/image'; | ||||
|         const apiKey = Constants.ImgurApiKey; | ||||
| 
 | ||||
|         const formData = new FormData(); | ||||
|         formData.append('image', blob); | ||||
|         formData.append("title", title); | ||||
|         const formData = new FormData() | ||||
|         formData.append("image", blob) | ||||
|         formData.append("title", title) | ||||
|         formData.append("description", description) | ||||
| 
 | ||||
|         const settings: RequestInit = { | ||||
|             method: 'POST', | ||||
|             method: "POST", | ||||
|             body: formData, | ||||
|             redirect: 'follow', | ||||
|             redirect: "follow", | ||||
|             headers: new Headers({ | ||||
|                 Authorization: `Client-ID ${apiKey}`, | ||||
|                 Accept: 'application/json', | ||||
|                 Accept: "application/json", | ||||
|             }), | ||||
|         }; | ||||
|         } | ||||
| 
 | ||||
|         // Response contains stringified JSON
 | ||||
|         // Image URL available at response.data.link
 | ||||
|         fetch(apiUrl, settings).then(async function (response) { | ||||
|         fetch(apiUrl, settings) | ||||
|             .then(async function (response) { | ||||
|                 const content = await response.json() | ||||
|             await handleSuccessfullUpload(content.data.link); | ||||
|         }).catch((reason) => { | ||||
|             console.log("Uploading to IMGUR failed", reason); | ||||
|                 await handleSuccessfullUpload(content.data.link) | ||||
|             }) | ||||
|             .catch((reason) => { | ||||
|                 console.log("Uploading to IMGUR failed", reason) | ||||
|                 // @ts-ignore
 | ||||
|             onFail(reason); | ||||
|         }); | ||||
|                 onFail(reason) | ||||
|             }) | ||||
|     } | ||||
| 
 | ||||
|     SourceIcon(): BaseUIElement { | ||||
|         return undefined; | ||||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|     public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { | ||||
|         if (Imgur.defaultValuePrefix.some(prefix => value.startsWith(prefix))) { | ||||
|             return [Promise.resolve({ | ||||
|         if (Imgur.defaultValuePrefix.some((prefix) => value.startsWith(prefix))) { | ||||
|             return [ | ||||
|                 Promise.resolve({ | ||||
|                     url: value, | ||||
|                     key: key, | ||||
|                 provider: this | ||||
|             })] | ||||
|                     provider: this, | ||||
|                 }), | ||||
|             ] | ||||
|         } | ||||
|         return [] | ||||
|     } | ||||
|  | @ -103,29 +115,27 @@ export class Imgur extends ImageProvider { | |||
|      * expected.artist = "Pieter Vander Vennet" | ||||
|      * licenseInfo // => expected
 | ||||
|      */ | ||||
|     public async DownloadAttribution (url: string) : Promise<LicenseInfo> { | ||||
|         const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0]; | ||||
|     public async DownloadAttribution(url: string): Promise<LicenseInfo> { | ||||
|         const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0] | ||||
| 
 | ||||
|         const apiUrl = 'https://api.imgur.com/3/image/' + hash; | ||||
|         const response = await Utils.downloadJsonCached(apiUrl, 365*24*60*60, | ||||
|             {Authorization: 'Client-ID ' + Constants.ImgurApiKey}) | ||||
|         const apiUrl = "https://api.imgur.com/3/image/" + hash | ||||
|         const response = await Utils.downloadJsonCached(apiUrl, 365 * 24 * 60 * 60, { | ||||
|             Authorization: "Client-ID " + Constants.ImgurApiKey, | ||||
|         }) | ||||
| 
 | ||||
|         const descr: string = response.data.description ?? ""; | ||||
|         const data: any = {}; | ||||
|         const descr: string = response.data.description ?? "" | ||||
|         const data: any = {} | ||||
|         for (const tag of descr.split("\n")) { | ||||
|             const kv = tag.split(":"); | ||||
|             const k = kv[0]; | ||||
|             data[k] = kv[1]?.replace(/\r/g, ""); | ||||
|             const kv = tag.split(":") | ||||
|             const k = kv[0] | ||||
|             data[k] = kv[1]?.replace(/\r/g, "") | ||||
|         } | ||||
| 
 | ||||
|         const licenseInfo = new LicenseInfo() | ||||
| 
 | ||||
|         const licenseInfo = new LicenseInfo(); | ||||
| 
 | ||||
|         licenseInfo.licenseShortName = data.license; | ||||
|         licenseInfo.artist = data.author; | ||||
|         licenseInfo.licenseShortName = data.license | ||||
|         licenseInfo.artist = data.author | ||||
| 
 | ||||
|         return licenseInfo | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,16 +1,15 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Imgur} from "./Imgur"; | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import { Imgur } from "./Imgur" | ||||
| 
 | ||||
| export default class ImgurUploader { | ||||
| 
 | ||||
|     public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([]); | ||||
|     public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([]); | ||||
|     public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([]); | ||||
|     public maxFileSizeInMegabytes = 10; | ||||
|     private readonly _handleSuccessUrl: (string) => Promise<void>; | ||||
|     public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([]) | ||||
|     public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([]) | ||||
|     public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([]) | ||||
|     public maxFileSizeInMegabytes = 10 | ||||
|     private readonly _handleSuccessUrl: (string) => Promise<void> | ||||
| 
 | ||||
|     constructor(handleSuccessUrl: (string) => Promise<void>) { | ||||
|         this._handleSuccessUrl = handleSuccessUrl; | ||||
|         this._handleSuccessUrl = handleSuccessUrl | ||||
|     } | ||||
| 
 | ||||
|     public uploadMany(title: string, description: string, files: FileList): void { | ||||
|  | @ -19,25 +18,26 @@ export default class ImgurUploader { | |||
|         } | ||||
|         this.queue.ping() | ||||
| 
 | ||||
|         const self = this; | ||||
|         const self = this | ||||
|         this.queue.setData([...self.queue.data]) | ||||
|         Imgur.uploadMultiple(title, | ||||
|         Imgur.uploadMultiple( | ||||
|             title, | ||||
|             description, | ||||
|             files, | ||||
|             async function (url) { | ||||
|                 console.log("File saved at", url); | ||||
|                 console.log("File saved at", url) | ||||
|                 self.success.data.push(url) | ||||
|                 self.success.ping(); | ||||
|                 await self._handleSuccessUrl(url); | ||||
|                 self.success.ping() | ||||
|                 await self._handleSuccessUrl(url) | ||||
|             }, | ||||
|             function () { | ||||
|                 console.log("All uploads completed"); | ||||
|                 console.log("All uploads completed") | ||||
|             }, | ||||
| 
 | ||||
|             function (failReason) { | ||||
|                 console.log("Upload failed due to ", failReason) | ||||
|                 self.failed.setData([...self.failed.data, failReason]) | ||||
|             } | ||||
|         ); | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -1,12 +1,12 @@ | |||
| export class LicenseInfo { | ||||
|     title: string = "" | ||||
|     artist: string = ""; | ||||
|     license: string = undefined; | ||||
|     licenseShortName: string = ""; | ||||
|     usageTerms: string = ""; | ||||
|     attributionRequired: boolean = false; | ||||
|     copyrighted: boolean = false; | ||||
|     credit: string = ""; | ||||
|     description: string = ""; | ||||
|     artist: string = "" | ||||
|     license: string = undefined | ||||
|     licenseShortName: string = "" | ||||
|     usageTerms: string = "" | ||||
|     attributionRequired: boolean = false | ||||
|     copyrighted: boolean = false | ||||
|     credit: string = "" | ||||
|     description: string = "" | ||||
|     informationLocation: URL = undefined | ||||
| } | ||||
|  | @ -1,15 +1,20 @@ | |||
| import ImageProvider, {ProvidedImage} from "./ImageProvider"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import Svg from "../../Svg"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {LicenseInfo} from "./LicenseInfo"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||
| import BaseUIElement from "../../UI/BaseUIElement" | ||||
| import Svg from "../../Svg" | ||||
| import { Utils } from "../../Utils" | ||||
| import { LicenseInfo } from "./LicenseInfo" | ||||
| import Constants from "../../Models/Constants" | ||||
| 
 | ||||
| export class Mapillary extends ImageProvider { | ||||
| 
 | ||||
|     public static readonly singleton = new Mapillary(); | ||||
|     public static readonly singleton = new Mapillary() | ||||
|     private static readonly valuePrefix = "https://a.mapillary.com" | ||||
|     public static readonly valuePrefixes = [Mapillary.valuePrefix, "http://mapillary.com", "https://mapillary.com", "http://www.mapillary.com", "https://www.mapillary.com"] | ||||
|     public static readonly valuePrefixes = [ | ||||
|         Mapillary.valuePrefix, | ||||
|         "http://mapillary.com", | ||||
|         "https://mapillary.com", | ||||
|         "http://www.mapillary.com", | ||||
|         "https://www.mapillary.com", | ||||
|     ] | ||||
|     defaultKeyPrefixes = ["mapillary", "image"] | ||||
| 
 | ||||
|     /** | ||||
|  | @ -28,9 +33,9 @@ export class Mapillary extends ImageProvider { | |||
|             const aUrl = new URL(a) | ||||
|             const bUrl = new URL(b) | ||||
|             if (aUrl.host !== bUrl.host || aUrl.pathname !== bUrl.pathname) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
|             let allSame = true; | ||||
|             let allSame = true | ||||
|             aUrl.searchParams.forEach((value, key) => { | ||||
|                 if (key === "stp") { | ||||
|                     // This is the key indicating the image size on mapillary; we ignore it
 | ||||
|  | @ -41,20 +46,18 @@ export class Mapillary extends ImageProvider { | |||
|                     return | ||||
|                 } | ||||
|             }) | ||||
|             return allSame; | ||||
| 
 | ||||
|             return allSame | ||||
|         } catch (e) { | ||||
|             console.debug("Could not compare ", a, "and", b, "due to", e) | ||||
|         } | ||||
|         return false; | ||||
| 
 | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the correct key for API v4.0 | ||||
|      */ | ||||
|     private static ExtractKeyFromURL(value: string): number { | ||||
|         let key: string; | ||||
|         let key: string | ||||
| 
 | ||||
|         const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/) | ||||
|         if (newApiFormat !== null) { | ||||
|  | @ -62,7 +65,7 @@ export class Mapillary extends ImageProvider { | |||
|         } else if (value.startsWith(Mapillary.valuePrefix)) { | ||||
|             key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1) | ||||
|         } else if (value.match("[0-9]*")) { | ||||
|             key = value; | ||||
|             key = value | ||||
|         } | ||||
| 
 | ||||
|         const keyAsNumber = Number(key) | ||||
|  | @ -74,7 +77,7 @@ export class Mapillary extends ImageProvider { | |||
|     } | ||||
| 
 | ||||
|     SourceIcon(backlinkSource?: string): BaseUIElement { | ||||
|         return Svg.mapillary_svg(); | ||||
|         return Svg.mapillary_svg() | ||||
|     } | ||||
| 
 | ||||
|     async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { | ||||
|  | @ -83,26 +86,30 @@ export class Mapillary extends ImageProvider { | |||
| 
 | ||||
|     public async DownloadAttribution(url: string): Promise<LicenseInfo> { | ||||
|         const license = new LicenseInfo() | ||||
|         license.artist = "Contributor name unavailable"; | ||||
|         license.license = "CC BY-SA 4.0"; | ||||
|         license.artist = "Contributor name unavailable" | ||||
|         license.license = "CC BY-SA 4.0" | ||||
|         // license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
 | ||||
|         license.attributionRequired = true; | ||||
|         license.attributionRequired = true | ||||
|         return license | ||||
|     } | ||||
| 
 | ||||
|     private async PrepareUrlAsync(key: string, value: string): Promise<ProvidedImage> { | ||||
|         const mapillaryId = Mapillary.ExtractKeyFromURL(value) | ||||
|         if (mapillaryId === undefined) { | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         const metadataUrl = 'https://graph.mapillary.com/' + mapillaryId + '?fields=thumb_1024_url&&access_token=' + Constants.mapillary_client_token_v4; | ||||
|         const response = await Utils.downloadJsonCached(metadataUrl,60*60) | ||||
|         const url = <string>response["thumb_1024_url"]; | ||||
|         const metadataUrl = | ||||
|             "https://graph.mapillary.com/" + | ||||
|             mapillaryId + | ||||
|             "?fields=thumb_1024_url&&access_token=" + | ||||
|             Constants.mapillary_client_token_v4 | ||||
|         const response = await Utils.downloadJsonCached(metadataUrl, 60 * 60) | ||||
|         const url = <string>response["thumb_1024_url"] | ||||
|         return { | ||||
|             url: url, | ||||
|             provider: this, | ||||
|             key: key | ||||
|             key: key, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,11 +1,10 @@ | |||
| import ImageProvider, {ProvidedImage} from "./ImageProvider"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import Svg from "../../Svg"; | ||||
| import {WikimediaImageProvider} from "./WikimediaImageProvider"; | ||||
| import Wikidata from "../Web/Wikidata"; | ||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||
| import BaseUIElement from "../../UI/BaseUIElement" | ||||
| import Svg from "../../Svg" | ||||
| import { WikimediaImageProvider } from "./WikimediaImageProvider" | ||||
| import Wikidata from "../Web/Wikidata" | ||||
| 
 | ||||
| export class WikidataImageProvider extends ImageProvider { | ||||
| 
 | ||||
|     public static readonly singleton = new WikidataImageProvider() | ||||
|     public readonly defaultKeyPrefixes = ["wikidata"] | ||||
| 
 | ||||
|  | @ -14,7 +13,7 @@ export class WikidataImageProvider extends ImageProvider { | |||
|     } | ||||
| 
 | ||||
|     public SourceIcon(backlinkSource?: string): BaseUIElement { | ||||
|         throw Svg.wikidata_svg(); | ||||
|         throw Svg.wikidata_svg() | ||||
|     } | ||||
| 
 | ||||
|     public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { | ||||
|  | @ -39,7 +38,10 @@ export class WikidataImageProvider extends ImageProvider { | |||
|         } | ||||
| 
 | ||||
|         const commons = entity.commons | ||||
|         if (commons !== undefined && (commons.startsWith("Category:") || commons.startsWith("File:"))) { | ||||
|         if ( | ||||
|             commons !== undefined && | ||||
|             (commons.startsWith("Category:") || commons.startsWith("File:")) | ||||
|         ) { | ||||
|             const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, commons) | ||||
|             allImages.push(...promises) | ||||
|         } | ||||
|  | @ -47,7 +49,6 @@ export class WikidataImageProvider extends ImageProvider { | |||
|     } | ||||
| 
 | ||||
|     public DownloadAttribution(url: string): Promise<any> { | ||||
|         throw new Error("Method not implemented; shouldn't be needed!"); | ||||
|         throw new Error("Method not implemented; shouldn't be needed!") | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,45 +1,47 @@ | |||
| import ImageProvider, {ProvidedImage} from "./ImageProvider"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import Svg from "../../Svg"; | ||||
| import Link from "../../UI/Base/Link"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {LicenseInfo} from "./LicenseInfo"; | ||||
| import Wikimedia from "../Web/Wikimedia"; | ||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||
| import BaseUIElement from "../../UI/BaseUIElement" | ||||
| import Svg from "../../Svg" | ||||
| import Link from "../../UI/Base/Link" | ||||
| import { Utils } from "../../Utils" | ||||
| import { LicenseInfo } from "./LicenseInfo" | ||||
| import Wikimedia from "../Web/Wikimedia" | ||||
| 
 | ||||
| /** | ||||
|  * This module provides endpoints for wikimedia and others | ||||
|  */ | ||||
| export class WikimediaImageProvider extends ImageProvider { | ||||
| 
 | ||||
| 
 | ||||
|     public static readonly singleton = new WikimediaImageProvider(); | ||||
|     public static readonly commonsPrefixes = ["https://commons.wikimedia.org/wiki/", "https://upload.wikimedia.org", "File:"] | ||||
|     public static readonly singleton = new WikimediaImageProvider() | ||||
|     public static readonly commonsPrefixes = [ | ||||
|         "https://commons.wikimedia.org/wiki/", | ||||
|         "https://upload.wikimedia.org", | ||||
|         "File:", | ||||
|     ] | ||||
|     private readonly commons_key = "wikimedia_commons" | ||||
|     public readonly defaultKeyPrefixes = [this.commons_key, "image"] | ||||
| 
 | ||||
|     private constructor() { | ||||
|         super(); | ||||
|         super() | ||||
|     } | ||||
| 
 | ||||
|     private static ExtractFileName(url: string) { | ||||
|         if (!url.startsWith("http")) { | ||||
|             return url; | ||||
|             return url | ||||
|         } | ||||
|         const path = new URL(url).pathname | ||||
|         return path.substring(path.lastIndexOf("/") + 1); | ||||
| 
 | ||||
|         return path.substring(path.lastIndexOf("/") + 1) | ||||
|     } | ||||
| 
 | ||||
|     private static PrepareUrl(value: string): string { | ||||
| 
 | ||||
|         if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) { | ||||
|             return value; | ||||
|             return value | ||||
|         } | ||||
|         return (`https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(value)}?width=500&height=400`) | ||||
|         return `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent( | ||||
|             value | ||||
|         )}?width=500&height=400` | ||||
|     } | ||||
| 
 | ||||
|     private static startsWithCommonsPrefix(value: string): boolean { | ||||
|         return WikimediaImageProvider.commonsPrefixes.some(prefix => value.startsWith(prefix)) | ||||
|         return WikimediaImageProvider.commonsPrefixes.some((prefix) => value.startsWith(prefix)) | ||||
|     } | ||||
| 
 | ||||
|     private static removeCommonsPrefix(value: string): string { | ||||
|  | @ -49,7 +51,7 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|             if (!value.startsWith("File:")) { | ||||
|                 value = "File:" + value | ||||
|             } | ||||
|             return value; | ||||
|             return value | ||||
|         } | ||||
| 
 | ||||
|         for (const prefix of WikimediaImageProvider.commonsPrefixes) { | ||||
|  | @ -61,21 +63,20 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|                 return part | ||||
|             } | ||||
|         } | ||||
|         return value; | ||||
|         return value | ||||
|     } | ||||
| 
 | ||||
|     SourceIcon(backlink: string): BaseUIElement { | ||||
|         const img = Svg.wikimedia_commons_white_svg() | ||||
|             .SetStyle("width:2em;height: 2em"); | ||||
|         const img = Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em") | ||||
|         if (backlink === undefined) { | ||||
|             return img | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         return new Link(Svg.wikimedia_commons_white_img, | ||||
|             `https://commons.wikimedia.org/wiki/${backlink}`, true) | ||||
| 
 | ||||
| 
 | ||||
|         return new Link( | ||||
|             Svg.wikimedia_commons_white_img, | ||||
|             `https://commons.wikimedia.org/wiki/${backlink}`, | ||||
|             true | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     public PrepUrl(value: string): ProvidedImage { | ||||
|  | @ -99,7 +100,9 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|         value = WikimediaImageProvider.removeCommonsPrefix(value) | ||||
|         if (value.startsWith("Category:")) { | ||||
|             const urls = await Wikimedia.GetCategoryContents(value) | ||||
|             return urls.filter(url => url.startsWith("File:")).map(image => Promise.resolve(this.UrlForImage(image))) | ||||
|             return urls | ||||
|                 .filter((url) => url.startsWith("File:")) | ||||
|                 .map((image) => Promise.resolve(this.UrlForImage(image))) | ||||
|         } | ||||
|         if (value.startsWith("File:")) { | ||||
|             return [Promise.resolve(this.UrlForImage(value))] | ||||
|  | @ -116,24 +119,30 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|         filename = WikimediaImageProvider.ExtractFileName(filename) | ||||
| 
 | ||||
|         if (filename === "") { | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         const url = "https://en.wikipedia.org/w/" + | ||||
|         const url = | ||||
|             "https://en.wikipedia.org/w/" + | ||||
|             "api.php?action=query&prop=imageinfo&iiprop=extmetadata&" + | ||||
|             "titles=" + filename + | ||||
|             "&format=json&origin=*"; | ||||
|         const data = await Utils.downloadJsonCached(url,365*24*60*60) | ||||
|         const licenseInfo = new LicenseInfo(); | ||||
|             "titles=" + | ||||
|             filename + | ||||
|             "&format=json&origin=*" | ||||
|         const data = await Utils.downloadJsonCached(url, 365 * 24 * 60 * 60) | ||||
|         const licenseInfo = new LicenseInfo() | ||||
|         const pageInfo = data.query.pages[-1] | ||||
|         if (pageInfo === undefined) { | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         const license = (pageInfo.imageinfo ?? [])[0]?.extmetadata; | ||||
|         const license = (pageInfo.imageinfo ?? [])[0]?.extmetadata | ||||
|         if (license === undefined) { | ||||
|             console.warn("The file", filename, "has no usable metedata or license attached... Please fix the license info file yourself!") | ||||
|             return undefined; | ||||
|             console.warn( | ||||
|                 "The file", | ||||
|                 filename, | ||||
|                 "has no usable metedata or license attached... Please fix the license info file yourself!" | ||||
|             ) | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         let title = pageInfo.title | ||||
|  | @ -145,26 +154,22 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|         } | ||||
| 
 | ||||
|         licenseInfo.title = title | ||||
|         licenseInfo.artist = license.Artist?.value; | ||||
|         licenseInfo.license = license.License?.value; | ||||
|         licenseInfo.copyrighted = license.Copyrighted?.value; | ||||
|         licenseInfo.attributionRequired = license.AttributionRequired?.value; | ||||
|         licenseInfo.usageTerms = license.UsageTerms?.value; | ||||
|         licenseInfo.licenseShortName = license.LicenseShortName?.value; | ||||
|         licenseInfo.credit = license.Credit?.value; | ||||
|         licenseInfo.description = license.ImageDescription?.value; | ||||
|         licenseInfo.informationLocation = new URL("https://en.wikipedia.org/wiki/"+pageInfo.title) | ||||
|         return licenseInfo; | ||||
| 
 | ||||
|         licenseInfo.artist = license.Artist?.value | ||||
|         licenseInfo.license = license.License?.value | ||||
|         licenseInfo.copyrighted = license.Copyrighted?.value | ||||
|         licenseInfo.attributionRequired = license.AttributionRequired?.value | ||||
|         licenseInfo.usageTerms = license.UsageTerms?.value | ||||
|         licenseInfo.licenseShortName = license.LicenseShortName?.value | ||||
|         licenseInfo.credit = license.Credit?.value | ||||
|         licenseInfo.description = license.ImageDescription?.value | ||||
|         licenseInfo.informationLocation = new URL("https://en.wikipedia.org/wiki/" + pageInfo.title) | ||||
|         return licenseInfo | ||||
|     } | ||||
| 
 | ||||
|     private UrlForImage(image: string): ProvidedImage { | ||||
|         if (!image.startsWith("File:")) { | ||||
|             image = "File:" + image | ||||
|         } | ||||
|         return {url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this} | ||||
|         return { url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,23 +1,23 @@ | |||
| import Constants from "../Models/Constants"; | ||||
| import Constants from "../Models/Constants" | ||||
| 
 | ||||
| export default class Maproulette { | ||||
|     /** | ||||
|      * The API endpoint to use | ||||
|      */ | ||||
|   endpoint: string; | ||||
|     endpoint: string | ||||
| 
 | ||||
|     /** | ||||
|      * The API key to use for all requests | ||||
|      */ | ||||
|   private apiKey: string; | ||||
|     private apiKey: string | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a new Maproulette instance | ||||
|      * @param endpoint The API endpoint to use | ||||
|      */ | ||||
|     constructor(endpoint: string = "https://maproulette.org/api/v2") { | ||||
|     this.endpoint = endpoint; | ||||
|     this.apiKey = Constants.MaprouletteApiKey; | ||||
|         this.endpoint = endpoint | ||||
|         this.apiKey = Constants.MaprouletteApiKey | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -29,11 +29,11 @@ export default class Maproulette { | |||
|             method: "PUT", | ||||
|             headers: { | ||||
|                 "Content-Type": "application/json", | ||||
|         "apiKey": this.apiKey, | ||||
|                 apiKey: this.apiKey, | ||||
|             }, | ||||
|     }); | ||||
|         }) | ||||
|         if (response.status !== 304) { | ||||
|       console.log(`Failed to close task: ${response.status}`); | ||||
|             console.log(`Failed to close task: ${response.status}`) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,8 +1,7 @@ | |||
| import SimpleMetaTaggers, {SimpleMetaTagger} from "./SimpleMetaTagger"; | ||||
| import {ExtraFuncParams, ExtraFunctions} from "./ExtraFunctions"; | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| import {ElementStorage} from "./ElementStorage"; | ||||
| 
 | ||||
| import SimpleMetaTaggers, { SimpleMetaTagger } from "./SimpleMetaTagger" | ||||
| import { ExtraFuncParams, ExtraFunctions } from "./ExtraFunctions" | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||
| import { ElementStorage } from "./ElementStorage" | ||||
| 
 | ||||
| /** | ||||
|  * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... | ||||
|  | @ -10,10 +9,8 @@ import {ElementStorage} from "./ElementStorage"; | |||
|  * All metatags start with an underscore | ||||
|  */ | ||||
| export default class MetaTagging { | ||||
| 
 | ||||
| 
 | ||||
|     private static errorPrintCount = 0; | ||||
|     private static readonly stopErrorOutputAt = 10; | ||||
|     private static errorPrintCount = 0 | ||||
|     private static readonly stopErrorOutputAt = 10 | ||||
|     private static retaggingFuncCache = new Map<string, ((feature: any) => void)[]>() | ||||
| 
 | ||||
|     /** | ||||
|  | @ -22,17 +19,19 @@ export default class MetaTagging { | |||
|      * | ||||
|      * Returns true if at least one feature has changed properties | ||||
|      */ | ||||
|     public static addMetatags(features: { feature: any; freshness: Date }[], | ||||
|     public static addMetatags( | ||||
|         features: { feature: any; freshness: Date }[], | ||||
|         params: ExtraFuncParams, | ||||
|         layer: LayerConfig, | ||||
|         state?: { allElements?: ElementStorage }, | ||||
|         options?: { | ||||
|                                   includeDates?: true | boolean, | ||||
|                                   includeNonDates?: true | boolean, | ||||
|             includeDates?: true | boolean | ||||
|             includeNonDates?: true | boolean | ||||
|             evaluateStrict?: false | boolean | ||||
|                               }): boolean { | ||||
|         } | ||||
|     ): boolean { | ||||
|         if (features === undefined || features.length === 0) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         console.log("Recalculating metatags...") | ||||
|  | @ -52,35 +51,40 @@ export default class MetaTagging { | |||
|         // The calculated functions - per layer - which add the new keys
 | ||||
|         const layerFuncs = this.createRetaggingFunc(layer, state) | ||||
| 
 | ||||
|         let atLeastOneFeatureChanged = false; | ||||
|         let atLeastOneFeatureChanged = false | ||||
| 
 | ||||
|         for (let i = 0; i < features.length; i++) { | ||||
|             const ff = features[i]; | ||||
|             const ff = features[i] | ||||
|             const feature = ff.feature | ||||
|             const freshness = ff.freshness | ||||
|             let somethingChanged = false | ||||
|             let definedTags = new Set(Object.getOwnPropertyNames(feature.properties)) | ||||
|             for (const metatag of metatagsToApply) { | ||||
|                 try { | ||||
|                     if (!metatag.keys.some(key => feature.properties[key] === undefined)) { | ||||
|                     if (!metatag.keys.some((key) => feature.properties[key] === undefined)) { | ||||
|                         // All keys are already defined, we probably already ran this one
 | ||||
|                         continue | ||||
|                     } | ||||
| 
 | ||||
|                     if (metatag.isLazy) { | ||||
|                         if (!metatag.keys.some(key => !definedTags.has(key))) { | ||||
|                         if (!metatag.keys.some((key) => !definedTags.has(key))) { | ||||
|                             // All keys are defined - lets skip!
 | ||||
|                             continue | ||||
|                         } | ||||
|                         somethingChanged = true; | ||||
|                         somethingChanged = true | ||||
|                         metatag.applyMetaTagsOnFeature(feature, freshness, layer, state) | ||||
|                         if(options?.evaluateStrict){ | ||||
|                         if (options?.evaluateStrict) { | ||||
|                             for (const key of metatag.keys) { | ||||
|                                 feature.properties[key] | ||||
|                             } | ||||
|                         } | ||||
|                     } else { | ||||
|                         const newValueAdded = metatag.applyMetaTagsOnFeature(feature, freshness, layer, state) | ||||
|                         const newValueAdded = metatag.applyMetaTagsOnFeature( | ||||
|                             feature, | ||||
|                             freshness, | ||||
|                             layer, | ||||
|                             state | ||||
|                         ) | ||||
|                         /* Note that the expression: | ||||
|                          * `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)` | ||||
|                          * Is WRONG | ||||
|  | @ -91,12 +95,18 @@ export default class MetaTagging { | |||
|                         somethingChanged = newValueAdded || somethingChanged | ||||
|                     } | ||||
|                 } catch (e) { | ||||
|                     console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e, e.stack) | ||||
|                     console.error( | ||||
|                         "Could not calculate metatag for ", | ||||
|                         metatag.keys.join(","), | ||||
|                         ":", | ||||
|                         e, | ||||
|                         e.stack | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (layerFuncs !== undefined) { | ||||
|                 let retaggingChanged = false; | ||||
|                 let retaggingChanged = false | ||||
|                 try { | ||||
|                     retaggingChanged = layerFuncs(params, feature) | ||||
|                 } catch (e) { | ||||
|  | @ -113,42 +123,62 @@ export default class MetaTagging { | |||
|         return atLeastOneFeatureChanged | ||||
|     } | ||||
| 
 | ||||
|     private static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] { | ||||
|         const functions: ((feature: any) => any)[] = []; | ||||
|     private static createFunctionsForFeature( | ||||
|         layerId: string, | ||||
|         calculatedTags: [string, string, boolean][] | ||||
|     ): ((feature: any) => void)[] { | ||||
|         const functions: ((feature: any) => any)[] = [] | ||||
|         for (const entry of calculatedTags) { | ||||
|             const key = entry[0] | ||||
|             const code = entry[1]; | ||||
|             const code = entry[1] | ||||
|             const isStrict = entry[2] | ||||
|             if (code === undefined) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             const calculateAndAssign: ((feat: any) => any) = (feat) => { | ||||
|             const calculateAndAssign: (feat: any) => any = (feat) => { | ||||
|                 try { | ||||
|                     let result = new Function("feat", "return " + code + ";")(feat); | ||||
|                     let result = new Function("feat", "return " + code + ";")(feat) | ||||
|                     if (result === "") { | ||||
|                         result === undefined | ||||
|                     } | ||||
|                     if (result !== undefined && typeof result !== "string") { | ||||
|                         // Make sure it is a string!
 | ||||
|                         result = JSON.stringify(result); | ||||
|                         result = JSON.stringify(result) | ||||
|                     } | ||||
|                     delete feat.properties[key] | ||||
|                     feat.properties[key] = result; | ||||
|                     feat.properties[key] = result | ||||
|                     return result | ||||
|                 } catch (e) { | ||||
|                     if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { | ||||
|                         console.warn("Could not calculate a " + (isStrict ? "strict " : "") + " calculated tag for key " + key + " defined by " + code + " (in layer" + layerId + ") due to \n" + e + "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e, e.stack) | ||||
|                         MetaTagging.errorPrintCount++; | ||||
|                         console.warn( | ||||
|                             "Could not calculate a " + | ||||
|                                 (isStrict ? "strict " : "") + | ||||
|                                 " calculated tag for key " + | ||||
|                                 key + | ||||
|                                 " defined by " + | ||||
|                                 code + | ||||
|                                 " (in layer" + | ||||
|                                 layerId + | ||||
|                                 ") due to \n" + | ||||
|                                 e + | ||||
|                                 "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", | ||||
|                             e, | ||||
|                             e.stack | ||||
|                         ) | ||||
|                         MetaTagging.errorPrintCount++ | ||||
|                         if (MetaTagging.errorPrintCount == MetaTagging.stopErrorOutputAt) { | ||||
|                             console.error("Got ", MetaTagging.stopErrorOutputAt, " errors calculating this metatagging - stopping output now") | ||||
|                             console.error( | ||||
|                                 "Got ", | ||||
|                                 MetaTagging.stopErrorOutputAt, | ||||
|                                 " errors calculating this metatagging - stopping output now" | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                     return undefined; | ||||
|                     return undefined | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             if (isStrict) { | ||||
|                 functions.push(calculateAndAssign) | ||||
|                 continue | ||||
|  | @ -162,15 +192,14 @@ export default class MetaTagging { | |||
|                     enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this
 | ||||
|                     get: function () { | ||||
|                         return calculateAndAssign(feature) | ||||
|                     } | ||||
|                     }, | ||||
|                 }) | ||||
|                 return undefined | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             functions.push(f) | ||||
|         } | ||||
|         return functions; | ||||
|         return functions | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -179,39 +208,37 @@ export default class MetaTagging { | |||
|      * @param state | ||||
|      * @private | ||||
|      */ | ||||
|     private static createRetaggingFunc(layer: LayerConfig, state): | ||||
|         ((params: ExtraFuncParams, feature: any) => boolean) { | ||||
| 
 | ||||
|         const calculatedTags: [string, string, boolean][] = layer.calculatedTags; | ||||
|     private static createRetaggingFunc( | ||||
|         layer: LayerConfig, | ||||
|         state | ||||
|     ): (params: ExtraFuncParams, feature: any) => boolean { | ||||
|         const calculatedTags: [string, string, boolean][] = layer.calculatedTags | ||||
|         if (calculatedTags === undefined || calculatedTags.length === 0) { | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         let functions: ((feature: any) => void)[] = MetaTagging.retaggingFuncCache.get(layer.id); | ||||
|         let functions: ((feature: any) => void)[] = MetaTagging.retaggingFuncCache.get(layer.id) | ||||
|         if (functions === undefined) { | ||||
|             functions = MetaTagging.createFunctionsForFeature(layer.id, calculatedTags) | ||||
|             MetaTagging.retaggingFuncCache.set(layer.id, functions) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         return (params: ExtraFuncParams, feature) => { | ||||
|             const tags = feature.properties | ||||
|             if (tags === undefined) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 ExtraFunctions.FullPatchFeature(params, feature); | ||||
|                 ExtraFunctions.FullPatchFeature(params, feature) | ||||
|                 for (const f of functions) { | ||||
|                     f(feature); | ||||
|                     f(feature) | ||||
|                 } | ||||
|                 state?.allElements?.getEventSourceById(feature.properties.id)?.ping(); | ||||
|                 state?.allElements?.getEventSourceById(feature.properties.id)?.ping() | ||||
|             } catch (e) { | ||||
|                 console.error("Invalid syntax in calculated tags or some other error: ", e) | ||||
|             } | ||||
|             return true; // Something changed
 | ||||
|             return true // Something changed
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,10 +1,9 @@ | |||
| import {OsmNode, OsmRelation, OsmWay} from "../OsmObject"; | ||||
| import { OsmNode, OsmRelation, OsmWay } from "../OsmObject" | ||||
| 
 | ||||
| /** | ||||
|  * Represents a single change to an object | ||||
|  */ | ||||
| export interface ChangeDescription { | ||||
| 
 | ||||
|     /** | ||||
|      * Metadata to be included in the changeset | ||||
|      */ | ||||
|  | @ -12,7 +11,7 @@ export interface ChangeDescription { | |||
|         /* | ||||
|          * The theme with which this changeset was made | ||||
|          */ | ||||
|         theme: string, | ||||
|         theme: string | ||||
|         /** | ||||
|          * The type of the change | ||||
|          */ | ||||
|  | @ -20,22 +19,22 @@ export interface ChangeDescription { | |||
|         /** | ||||
|          * THe motivation for the change, e.g. 'deleted because does not exist anymore' | ||||
|          */ | ||||
|         specialMotivation?: string, | ||||
|         specialMotivation?: string | ||||
|         /** | ||||
|          * Added by Changes.ts | ||||
|          */ | ||||
|         distanceToObject?: number | ||||
|     }, | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Identifier of the object | ||||
|      */ | ||||
|     type: "node" | "way" | "relation", | ||||
|     type: "node" | "way" | "relation" | ||||
|     /** | ||||
|      * Identifier of the object | ||||
|      * Negative for new objects | ||||
|      */ | ||||
|     id: number, | ||||
|     id: number | ||||
| 
 | ||||
|     /** | ||||
|      * All changes to tags | ||||
|  | @ -43,7 +42,7 @@ export interface ChangeDescription { | |||
|      * | ||||
|      * Note that this list will only contain the _changes_ to the tags, not the full set of tags | ||||
|      */ | ||||
|     tags?: { k: string, v: string }[], | ||||
|     tags?: { k: string; v: string }[] | ||||
| 
 | ||||
|     /** | ||||
|      * A change to the geometry: | ||||
|  | @ -51,16 +50,19 @@ export interface ChangeDescription { | |||
|      * 2) Change of way geometry | ||||
|      * 3) Change of relation members (untested) | ||||
|      */ | ||||
|     changes?: { | ||||
|         lat: number, | ||||
|     changes?: | ||||
|         | { | ||||
|               lat: number | ||||
|               lon: number | ||||
|     } | { | ||||
|           } | ||||
|         | { | ||||
|               /* Coordinates are only used for rendering. They should be LON, LAT | ||||
|                * */ | ||||
|               coordinates: [number, number][] | ||||
|         nodes: number[], | ||||
|     } | { | ||||
|         members: { type: "node" | "way" | "relation", ref: number, role: string }[] | ||||
|               nodes: number[] | ||||
|           } | ||||
|         | { | ||||
|               members: { type: "node" | "way" | "relation"; ref: number; role: string }[] | ||||
|           } | ||||
| 
 | ||||
|     /* | ||||
|  | @ -70,7 +72,6 @@ export interface ChangeDescription { | |||
| } | ||||
| 
 | ||||
| export class ChangeDescriptionTools { | ||||
| 
 | ||||
|     /** | ||||
|      * Rewrites all the ids in a changeDescription | ||||
|      * | ||||
|  | @ -130,44 +131,49 @@ export class ChangeDescriptionTools { | |||
|      * rewritten.changes["members"] // => [{type: "way", ref: 42, role: "outer"},{type: "way", ref: 48, role: "outer"}]
 | ||||
|      * | ||||
|      */ | ||||
|     public static rewriteIds(change: ChangeDescription, mappings: Map<string, string>): ChangeDescription { | ||||
|     public static rewriteIds( | ||||
|         change: ChangeDescription, | ||||
|         mappings: Map<string, string> | ||||
|     ): ChangeDescription { | ||||
|         const key = change.type + "/" + change.id | ||||
| 
 | ||||
|         const wayHasChangedNode = ((change.changes ?? {})["nodes"] ?? []).some(id => mappings.has("node/" + id)); | ||||
|         const relationHasChangedMembers = ((change.changes ?? {})["members"] ?? []) | ||||
|             .some((obj:{type: string, ref: number}) => mappings.has(obj.type+"/" + obj.ref)); | ||||
|         const wayHasChangedNode = ((change.changes ?? {})["nodes"] ?? []).some((id) => | ||||
|             mappings.has("node/" + id) | ||||
|         ) | ||||
|         const relationHasChangedMembers = ((change.changes ?? {})["members"] ?? []).some( | ||||
|             (obj: { type: string; ref: number }) => mappings.has(obj.type + "/" + obj.ref) | ||||
|         ) | ||||
| 
 | ||||
|         const hasSomeChange = mappings.has(key) | ||||
|             || wayHasChangedNode || relationHasChangedMembers | ||||
|         if(hasSomeChange){ | ||||
|             change = {...change} | ||||
|         const hasSomeChange = mappings.has(key) || wayHasChangedNode || relationHasChangedMembers | ||||
|         if (hasSomeChange) { | ||||
|             change = { ...change } | ||||
|         } | ||||
| 
 | ||||
|         if (mappings.has(key)) { | ||||
|             const [_, newId] = mappings.get(key).split("/") | ||||
|             change.id = Number.parseInt(newId) | ||||
|         } | ||||
|         if(wayHasChangedNode){ | ||||
|             change.changes = {...change.changes} | ||||
|             change.changes["nodes"] = change.changes["nodes"].map(id => { | ||||
|                 const key = "node/"+id | ||||
|                 if(!mappings.has(key)){ | ||||
|         if (wayHasChangedNode) { | ||||
|             change.changes = { ...change.changes } | ||||
|             change.changes["nodes"] = change.changes["nodes"].map((id) => { | ||||
|                 const key = "node/" + id | ||||
|                 if (!mappings.has(key)) { | ||||
|                     return id | ||||
|                 } | ||||
|                 const [_, newId] = mappings.get(key).split("/") | ||||
|                 return Number.parseInt(newId) | ||||
|             }) | ||||
|         } | ||||
|         if(relationHasChangedMembers){ | ||||
|             change.changes = {...change.changes} | ||||
|         if (relationHasChangedMembers) { | ||||
|             change.changes = { ...change.changes } | ||||
|             change.changes["members"] = change.changes["members"].map( | ||||
|                 (obj:{type: string, ref: number}) => { | ||||
|                     const key = obj.type+"/"+obj.ref; | ||||
|                     if(!mappings.has(key)){ | ||||
|                 (obj: { type: string; ref: number }) => { | ||||
|                     const key = obj.type + "/" + obj.ref | ||||
|                     if (!mappings.has(key)) { | ||||
|                         return obj | ||||
|                     } | ||||
|                     const [_, newId] = mappings.get(key).split("/") | ||||
|                     return {...obj, ref: Number.parseInt(newId)} | ||||
|                     return { ...obj, ref: Number.parseInt(newId) } | ||||
|                 } | ||||
|             ) | ||||
|         } | ||||
|  |  | |||
|  | @ -1,39 +1,42 @@ | |||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import OsmChangeAction from "./OsmChangeAction"; | ||||
| import {Changes} from "../Changes"; | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| import OsmChangeAction from "./OsmChangeAction" | ||||
| import { Changes } from "../Changes" | ||||
| 
 | ||||
| export default class ChangeLocationAction extends OsmChangeAction { | ||||
|     private readonly _id: number; | ||||
|     private readonly _newLonLat: [number, number]; | ||||
|     private readonly _meta: { theme: string; reason: string }; | ||||
|     private readonly _id: number | ||||
|     private readonly _newLonLat: [number, number] | ||||
|     private readonly _meta: { theme: string; reason: string } | ||||
| 
 | ||||
|     constructor(id: string, newLonLat: [number, number], meta: { | ||||
|         theme: string, | ||||
|     constructor( | ||||
|         id: string, | ||||
|         newLonLat: [number, number], | ||||
|         meta: { | ||||
|             theme: string | ||||
|             reason: string | ||||
|     }) { | ||||
|         super(id, true); | ||||
|         } | ||||
|     ) { | ||||
|         super(id, true) | ||||
|         if (!id.startsWith("node/")) { | ||||
|             throw "Invalid ID: only 'node/number' is accepted" | ||||
|         } | ||||
|         this._id = Number(id.substring("node/".length)) | ||||
|         this._newLonLat = newLonLat; | ||||
|         this._meta = meta; | ||||
|         this._newLonLat = newLonLat | ||||
|         this._meta = meta | ||||
|     } | ||||
| 
 | ||||
|     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
| 
 | ||||
|         const d: ChangeDescription = { | ||||
|             changes: { | ||||
|                 lat: this._newLonLat[1], | ||||
|                 lon: this._newLonLat[0] | ||||
|                 lon: this._newLonLat[0], | ||||
|             }, | ||||
|             type: "node", | ||||
|             id: this._id, meta: { | ||||
|             id: this._id, | ||||
|             meta: { | ||||
|                 changeType: "move", | ||||
|                 theme: this._meta.theme, | ||||
|                 specialMotivation: this._meta.reason | ||||
|             } | ||||
| 
 | ||||
|                 specialMotivation: this._meta.reason, | ||||
|             }, | ||||
|         } | ||||
| 
 | ||||
|         return [d] | ||||
|  |  | |||
|  | @ -1,65 +1,77 @@ | |||
| import OsmChangeAction from "./OsmChangeAction"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | ||||
| import {OsmTags} from "../../../Models/OsmFeature"; | ||||
| import OsmChangeAction from "./OsmChangeAction" | ||||
| import { Changes } from "../Changes" | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| import { TagsFilter } from "../../Tags/TagsFilter" | ||||
| import { OsmTags } from "../../../Models/OsmFeature" | ||||
| 
 | ||||
| export default class ChangeTagAction extends OsmChangeAction { | ||||
|     private readonly _elementId: string; | ||||
|     private readonly _tagsFilter: TagsFilter; | ||||
|     private readonly _currentTags: Record<string, string> | OsmTags; | ||||
|     private readonly _meta: { theme: string, changeType: string }; | ||||
|     private readonly _elementId: string | ||||
|     private readonly _tagsFilter: TagsFilter | ||||
|     private readonly _currentTags: Record<string, string> | OsmTags | ||||
|     private readonly _meta: { theme: string; changeType: string } | ||||
| 
 | ||||
|     constructor(elementId: string,  | ||||
|     constructor( | ||||
|         elementId: string, | ||||
|         tagsFilter: TagsFilter, | ||||
|                 currentTags: Record<string, string>, meta: { | ||||
|         theme: string, | ||||
|         currentTags: Record<string, string>, | ||||
|         meta: { | ||||
|             theme: string | ||||
|             changeType: "answer" | "soft-delete" | "add-image" | string | ||||
|     }) { | ||||
|         super(elementId, true); | ||||
|         this._elementId = elementId; | ||||
|         this._tagsFilter = tagsFilter; | ||||
|         this._currentTags = currentTags; | ||||
|         this._meta = meta; | ||||
|         } | ||||
|     ) { | ||||
|         super(elementId, true) | ||||
|         this._elementId = elementId | ||||
|         this._tagsFilter = tagsFilter | ||||
|         this._currentTags = currentTags | ||||
|         this._meta = meta | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Doublechecks that no stupid values are added | ||||
|      */ | ||||
|     private static checkChange(kv: { k: string, v: string }): { k: string, v: string } { | ||||
|         const key = kv.k; | ||||
|         const value = kv.v; | ||||
|     private static checkChange(kv: { k: string; v: string }): { k: string; v: string } { | ||||
|         const key = kv.k | ||||
|         const value = kv.v | ||||
|         if (key === undefined || key === null) { | ||||
|             console.error("Invalid key:", key); | ||||
|             return undefined; | ||||
|             console.error("Invalid key:", key) | ||||
|             return undefined | ||||
|         } | ||||
|         if (value === undefined || value === null) { | ||||
|             console.error("Invalid value for ", key, ":", value); | ||||
|             return undefined; | ||||
|             console.error("Invalid value for ", key, ":", value) | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         if (typeof value !== "string") { | ||||
|             console.error("Invalid value for ", key, "as it is not a string:", value) | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         if (key.startsWith(" ") || value.startsWith(" ") || value.endsWith(" ") || key.endsWith(" ")) { | ||||
|         if ( | ||||
|             key.startsWith(" ") || | ||||
|             value.startsWith(" ") || | ||||
|             value.endsWith(" ") || | ||||
|             key.endsWith(" ") | ||||
|         ) { | ||||
|             console.warn("Tag starts with or ends with a space - trimming anyway") | ||||
|         } | ||||
| 
 | ||||
|         return {k: key.trim(), v: value.trim()}; | ||||
|         return { k: key.trim(), v: value.trim() } | ||||
|     } | ||||
| 
 | ||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
|         const changedTags: { k: string, v: string }[] = this._tagsFilter.asChange(this._currentTags).map(ChangeTagAction.checkChange) | ||||
|         const changedTags: { k: string; v: string }[] = this._tagsFilter | ||||
|             .asChange(this._currentTags) | ||||
|             .map(ChangeTagAction.checkChange) | ||||
|         const typeId = this._elementId.split("/") | ||||
|         const type = typeId[0] | ||||
|         const id = Number(typeId  [1]) | ||||
|         return [{ | ||||
|         const id = Number(typeId[1]) | ||||
|         return [ | ||||
|             { | ||||
|                 type: <"node" | "way" | "relation">type, | ||||
|                 id: id, | ||||
|                 tags: changedTags, | ||||
|             meta: this._meta | ||||
|         }] | ||||
|                 meta: this._meta, | ||||
|             }, | ||||
|         ] | ||||
|     } | ||||
| } | ||||
|  | @ -1,64 +1,69 @@ | |||
| import {OsmCreateAction} from "./OsmChangeAction"; | ||||
| import {Tag} from "../../Tags/Tag"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import FeaturePipelineState from "../../State/FeaturePipelineState"; | ||||
| import FeatureSource from "../../FeatureSource/FeatureSource"; | ||||
| import CreateNewWayAction from "./CreateNewWayAction"; | ||||
| import CreateWayWithPointReuseAction, {MergePointConfig} from "./CreateWayWithPointReuseAction"; | ||||
| import {And} from "../../Tags/And"; | ||||
| import {TagUtils} from "../../Tags/TagUtils"; | ||||
| 
 | ||||
| import { OsmCreateAction } from "./OsmChangeAction" | ||||
| import { Tag } from "../../Tags/Tag" | ||||
| import { Changes } from "../Changes" | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| import FeaturePipelineState from "../../State/FeaturePipelineState" | ||||
| import FeatureSource from "../../FeatureSource/FeatureSource" | ||||
| import CreateNewWayAction from "./CreateNewWayAction" | ||||
| import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWithPointReuseAction" | ||||
| import { And } from "../../Tags/And" | ||||
| import { TagUtils } from "../../Tags/TagUtils" | ||||
| 
 | ||||
| /** | ||||
|  * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points | ||||
|  */ | ||||
| export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAction { | ||||
|     public newElementId: string = undefined; | ||||
|     public newElementIdNumber: number = undefined; | ||||
|     private readonly _tags: Tag[]; | ||||
|     public newElementId: string = undefined | ||||
|     public newElementIdNumber: number = undefined | ||||
|     private readonly _tags: Tag[] | ||||
|     private readonly createOuterWay: CreateWayWithPointReuseAction | ||||
|     private readonly createInnerWays: CreateNewWayAction[] | ||||
|     private readonly geojsonPreview: any; | ||||
|     private readonly theme: string; | ||||
|     private readonly changeType: "import" | "create" | string; | ||||
|     private readonly geojsonPreview: any | ||||
|     private readonly theme: string | ||||
|     private readonly changeType: "import" | "create" | string | ||||
| 
 | ||||
|     constructor(tags: Tag[], | ||||
|     constructor( | ||||
|         tags: Tag[], | ||||
|         outerRingCoordinates: [number, number][], | ||||
|         innerRingsCoordinates: [number, number][][], | ||||
|         state: FeaturePipelineState, | ||||
|         config: MergePointConfig[], | ||||
|         changeType: "import" | "create" | string | ||||
|     ) { | ||||
|         super(null, true); | ||||
|         this._tags = [...tags, new Tag("type", "multipolygon")]; | ||||
|         this.changeType = changeType; | ||||
|         super(null, true) | ||||
|         this._tags = [...tags, new Tag("type", "multipolygon")] | ||||
|         this.changeType = changeType | ||||
|         this.theme = state?.layoutToUse?.id ?? "" | ||||
|         this.createOuterWay = new CreateWayWithPointReuseAction([], outerRingCoordinates, state, config) | ||||
|         this.createInnerWays = innerRingsCoordinates.map(ringCoordinates => | ||||
|             new CreateNewWayAction([], | ||||
|                 ringCoordinates.map(([lon, lat]) => ({lat, lon})), | ||||
|                 {theme: state?.layoutToUse?.id})) | ||||
|         this.createOuterWay = new CreateWayWithPointReuseAction( | ||||
|             [], | ||||
|             outerRingCoordinates, | ||||
|             state, | ||||
|             config | ||||
|         ) | ||||
|         this.createInnerWays = innerRingsCoordinates.map( | ||||
|             (ringCoordinates) => | ||||
|                 new CreateNewWayAction( | ||||
|                     [], | ||||
|                     ringCoordinates.map(([lon, lat]) => ({ lat, lon })), | ||||
|                     { theme: state?.layoutToUse?.id } | ||||
|                 ) | ||||
|         ) | ||||
| 
 | ||||
|         this.geojsonPreview = { | ||||
|             type: "Feature", | ||||
|             properties: TagUtils.changeAsProperties(new And(this._tags).asChange({})), | ||||
|             geometry: { | ||||
|                 type: "Polygon", | ||||
|                 coordinates: [ | ||||
|                     outerRingCoordinates, | ||||
|                     ...innerRingsCoordinates | ||||
|                 ] | ||||
|                 coordinates: [outerRingCoordinates, ...innerRingsCoordinates], | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public async getPreview(): Promise<FeatureSource> { | ||||
|         const outerPreview = await this.createOuterWay.getPreview() | ||||
|         outerPreview.features.data.push({ | ||||
|             freshness: new Date(), | ||||
|             feature: this.geojsonPreview | ||||
|             feature: this.geojsonPreview, | ||||
|         }) | ||||
|         return outerPreview | ||||
|     } | ||||
|  | @ -66,13 +71,12 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct | |||
|     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
|         console.log("Running CMPWPRA") | ||||
|         const descriptions: ChangeDescription[] = [] | ||||
|         descriptions.push(...await this.createOuterWay.CreateChangeDescriptions(changes)); | ||||
|         descriptions.push(...(await this.createOuterWay.CreateChangeDescriptions(changes))) | ||||
|         for (const innerWay of this.createInnerWays) { | ||||
|             descriptions.push(...await innerWay.CreateChangeDescriptions(changes)) | ||||
|             descriptions.push(...(await innerWay.CreateChangeDescriptions(changes))) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         this.newElementIdNumber = changes.getNewID(); | ||||
|         this.newElementIdNumber = changes.getNewID() | ||||
|         this.newElementId = "relation/" + this.newElementIdNumber | ||||
|         descriptions.push({ | ||||
|             type: "relation", | ||||
|  | @ -80,24 +84,25 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct | |||
|             tags: new And(this._tags).asChange({}), | ||||
|             meta: { | ||||
|                 theme: this.theme, | ||||
|                 changeType: this.changeType | ||||
|                 changeType: this.changeType, | ||||
|             }, | ||||
|             changes: { | ||||
|                 members: [ | ||||
|                     { | ||||
|                         type: "way", | ||||
|                         ref: this.createOuterWay.newElementIdNumber, | ||||
|                         role: "outer" | ||||
|                         role: "outer", | ||||
|                     }, | ||||
|                     // @ts-ignore
 | ||||
|                     ...this.createInnerWays.map(a => ({type: "way", ref: a.newElementIdNumber, role: "inner"})) | ||||
|                 ] | ||||
|             } | ||||
|                     ...this.createInnerWays.map((a) => ({ | ||||
|                         type: "way", | ||||
|                         ref: a.newElementIdNumber, | ||||
|                         role: "inner", | ||||
|                     })), | ||||
|                 ], | ||||
|             }, | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         return descriptions | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,13 +1,12 @@ | |||
| import {Tag} from "../../Tags/Tag"; | ||||
| import {OsmCreateAction} from "./OsmChangeAction"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import {And} from "../../Tags/And"; | ||||
| import {OsmWay} from "../OsmObject"; | ||||
| import {GeoOperations} from "../../GeoOperations"; | ||||
| import { Tag } from "../../Tags/Tag" | ||||
| import { OsmCreateAction } from "./OsmChangeAction" | ||||
| import { Changes } from "../Changes" | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| import { And } from "../../Tags/And" | ||||
| import { OsmWay } from "../OsmObject" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| 
 | ||||
| export default class CreateNewNodeAction extends OsmCreateAction { | ||||
| 
 | ||||
|     /** | ||||
|      * Maps previously created points onto their assigned ID, to reuse the point if uplaoded | ||||
|      * "lat,lon" --> id | ||||
|  | @ -15,46 +14,47 @@ export default class CreateNewNodeAction extends OsmCreateAction { | |||
|     private static readonly previouslyCreatedPoints = new Map<string, number>() | ||||
|     public newElementId: string = undefined | ||||
|     public newElementIdNumber: number = undefined | ||||
|     private readonly _basicTags: Tag[]; | ||||
|     private readonly _lat: number; | ||||
|     private readonly _lon: number; | ||||
|     private readonly _snapOnto: OsmWay; | ||||
|     private readonly _reusePointDistance: number; | ||||
|     private meta: { changeType: "create" | "import"; theme: string; specialMotivation?: string }; | ||||
|     private readonly _reusePreviouslyCreatedPoint: boolean; | ||||
|     private readonly _basicTags: Tag[] | ||||
|     private readonly _lat: number | ||||
|     private readonly _lon: number | ||||
|     private readonly _snapOnto: OsmWay | ||||
|     private readonly _reusePointDistance: number | ||||
|     private meta: { changeType: "create" | "import"; theme: string; specialMotivation?: string } | ||||
|     private readonly _reusePreviouslyCreatedPoint: boolean | ||||
| 
 | ||||
|      | ||||
|     constructor(basicTags: Tag[], | ||||
|                 lat: number, lon: number, | ||||
|     constructor( | ||||
|         basicTags: Tag[], | ||||
|         lat: number, | ||||
|         lon: number, | ||||
|         options: { | ||||
|                     allowReuseOfPreviouslyCreatedPoints?: boolean, | ||||
|                     snapOnto?: OsmWay, | ||||
|                     reusePointWithinMeters?: number, | ||||
|                     theme: string, | ||||
|                     changeType: "create" | "import" | null, | ||||
|             allowReuseOfPreviouslyCreatedPoints?: boolean | ||||
|             snapOnto?: OsmWay | ||||
|             reusePointWithinMeters?: number | ||||
|             theme: string | ||||
|             changeType: "create" | "import" | null | ||||
|             specialMotivation?: string | ||||
|                 }) { | ||||
|         } | ||||
|     ) { | ||||
|         super(null, basicTags !== undefined && basicTags.length > 0) | ||||
|         this._basicTags = basicTags; | ||||
|         this._lat = lat; | ||||
|         this._lon = lon; | ||||
|         this._basicTags = basicTags | ||||
|         this._lat = lat | ||||
|         this._lon = lon | ||||
|         if (lat === undefined || lon === undefined) { | ||||
|             throw "Lat or lon are undefined!" | ||||
|         } | ||||
|         this._snapOnto = options?.snapOnto; | ||||
|         this._snapOnto = options?.snapOnto | ||||
|         this._reusePointDistance = options?.reusePointWithinMeters ?? 1 | ||||
|         this._reusePreviouslyCreatedPoint = options?.allowReuseOfPreviouslyCreatedPoints ?? (basicTags.length === 0) | ||||
|         this._reusePreviouslyCreatedPoint = | ||||
|             options?.allowReuseOfPreviouslyCreatedPoints ?? basicTags.length === 0 | ||||
|         this.meta = { | ||||
|             theme: options.theme, | ||||
|             changeType: options.changeType, | ||||
|             specialMotivation: options.specialMotivation | ||||
|             specialMotivation: options.specialMotivation, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
| 
 | ||||
|         if (this._reusePreviouslyCreatedPoint) { | ||||
| 
 | ||||
|             const key = this._lat + "," + this._lon | ||||
|             const prev = CreateNewNodeAction.previouslyCreatedPoints | ||||
|             if (prev.has(key)) { | ||||
|  | @ -64,17 +64,23 @@ export default class CreateNewNodeAction extends OsmCreateAction { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const id = changes.getNewID() | ||||
|         const properties = { | ||||
|             id: "node/" + id | ||||
|             id: "node/" + id, | ||||
|         } | ||||
|         this.setElementId(id) | ||||
|         for (const kv of this._basicTags) { | ||||
|             if (typeof kv.value !== "string") { | ||||
|                 throw "Invalid value: don't use non-string value in a preset. The tag "+kv.key+"="+kv.value+" is not a string, the value is a "+typeof kv.value | ||||
|                 throw ( | ||||
|                     "Invalid value: don't use non-string value in a preset. The tag " + | ||||
|                     kv.key + | ||||
|                     "=" + | ||||
|                     kv.value + | ||||
|                     " is not a string, the value is a " + | ||||
|                     typeof kv.value | ||||
|                 ) | ||||
|             } | ||||
|             properties[kv.key] = kv.value; | ||||
|             properties[kv.key] = kv.value | ||||
|         } | ||||
| 
 | ||||
|         const newPointChange: ChangeDescription = { | ||||
|  | @ -83,32 +89,31 @@ export default class CreateNewNodeAction extends OsmCreateAction { | |||
|             id: id, | ||||
|             changes: { | ||||
|                 lat: this._lat, | ||||
|                 lon: this._lon | ||||
|                 lon: this._lon, | ||||
|             }, | ||||
|             meta: this.meta | ||||
|             meta: this.meta, | ||||
|         } | ||||
|         if (this._snapOnto === undefined) { | ||||
|             return [newPointChange] | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         // Project the point onto the way
 | ||||
|         console.log("Snapping a node onto an existing way...") | ||||
|         const geojson = this._snapOnto.asGeoJson() | ||||
|         const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat]) | ||||
|        const projectedCoor=     <[number, number]>projected.geometry.coordinates | ||||
|         const projectedCoor = <[number, number]>projected.geometry.coordinates | ||||
|         const index = projected.properties.index | ||||
|         // We check that it isn't close to an already existing point
 | ||||
|         let reusedPointId = undefined; | ||||
|         let outerring : [number,number][]; | ||||
|         let reusedPointId = undefined | ||||
|         let outerring: [number, number][] | ||||
| 
 | ||||
|         if(geojson.geometry.type === "LineString"){ | ||||
|            outerring = <[number, number][]> geojson.geometry.coordinates | ||||
|         }else if(geojson.geometry.type === "Polygon"){ | ||||
|            outerring =<[number, number][]>  geojson.geometry.coordinates[0] | ||||
|         if (geojson.geometry.type === "LineString") { | ||||
|             outerring = <[number, number][]>geojson.geometry.coordinates | ||||
|         } else if (geojson.geometry.type === "Polygon") { | ||||
|             outerring = <[number, number][]>geojson.geometry.coordinates[0] | ||||
|         } | ||||
| 
 | ||||
|         const prev= outerring[index] | ||||
|         const prev = outerring[index] | ||||
|         if (GeoOperations.distanceBetween(prev, projectedCoor) < this._reusePointDistance) { | ||||
|             // We reuse this point instead!
 | ||||
|             reusedPointId = this._snapOnto.nodes[index] | ||||
|  | @ -120,15 +125,19 @@ export default class CreateNewNodeAction extends OsmCreateAction { | |||
|         } | ||||
|         if (reusedPointId !== undefined) { | ||||
|             this.setElementId(reusedPointId) | ||||
|             return [{ | ||||
|             return [ | ||||
|                 { | ||||
|                     tags: new And(this._basicTags).asChange(properties), | ||||
|                     type: "node", | ||||
|                     id: reusedPointId, | ||||
|                 meta: this.meta | ||||
|             }] | ||||
|                     meta: this.meta, | ||||
|                 }, | ||||
|             ] | ||||
|         } | ||||
| 
 | ||||
|         const locations = [...this._snapOnto.coordinates.map(([lat, lon]) =><[number,number]> [lon, lat])] | ||||
|         const locations = [ | ||||
|             ...this._snapOnto.coordinates.map(([lat, lon]) => <[number, number]>[lon, lat]), | ||||
|         ] | ||||
|         const ids = [...this._snapOnto.nodes] | ||||
| 
 | ||||
|         locations.splice(index + 1, 0, [this._lon, this._lat]) | ||||
|  | @ -142,15 +151,15 @@ export default class CreateNewNodeAction extends OsmCreateAction { | |||
|                 id: this._snapOnto.id, | ||||
|                 changes: { | ||||
|                     coordinates: locations, | ||||
|                     nodes: ids | ||||
|                     nodes: ids, | ||||
|                 }, | ||||
|                 meta: this.meta, | ||||
|             }, | ||||
|                 meta: this.meta | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| 
 | ||||
|     private setElementId(id: number) { | ||||
|         this.newElementIdNumber = id; | ||||
|         this.newElementIdNumber = id | ||||
|         this.newElementId = "node/" + id | ||||
|         if (!this._reusePreviouslyCreatedPoint) { | ||||
|             return | ||||
|  | @ -158,6 +167,4 @@ export default class CreateNewNodeAction extends OsmCreateAction { | |||
|         const key = this._lat + "," + this._lon | ||||
|         CreateNewNodeAction.previouslyCreatedPoints.set(key, id) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,19 +1,18 @@ | |||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import {OsmCreateAction} from "./OsmChangeAction"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {Tag} from "../../Tags/Tag"; | ||||
| import CreateNewNodeAction from "./CreateNewNodeAction"; | ||||
| import {And} from "../../Tags/And"; | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| import { OsmCreateAction } from "./OsmChangeAction" | ||||
| import { Changes } from "../Changes" | ||||
| import { Tag } from "../../Tags/Tag" | ||||
| import CreateNewNodeAction from "./CreateNewNodeAction" | ||||
| import { And } from "../../Tags/And" | ||||
| 
 | ||||
| export default class CreateNewWayAction extends OsmCreateAction { | ||||
|     public newElementId: string = undefined | ||||
|     public newElementIdNumber: number = undefined; | ||||
|     private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[]; | ||||
|     private readonly tags: Tag[]; | ||||
|     public newElementIdNumber: number = undefined | ||||
|     private readonly coordinates: { nodeId?: number; lat: number; lon: number }[] | ||||
|     private readonly tags: Tag[] | ||||
|     private readonly _options: { | ||||
|         theme: string | ||||
|     }; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /*** | ||||
|      * Creates a new way to upload to OSM | ||||
|  | @ -21,33 +20,44 @@ export default class CreateNewWayAction extends OsmCreateAction { | |||
|      * @param coordinates: the coordinates. Might have a nodeId, in this case, this node will be used | ||||
|      * @param options | ||||
|      */ | ||||
|     constructor(tags: Tag[], coordinates: ({ nodeId?: number, lat: number, lon: number })[], | ||||
|     constructor( | ||||
|         tags: Tag[], | ||||
|         coordinates: { nodeId?: number; lat: number; lon: number }[], | ||||
|         options: { | ||||
|             theme: string | ||||
|                 }) { | ||||
|         } | ||||
|     ) { | ||||
|         super(null, true) | ||||
|         this.coordinates = []; | ||||
|         this.coordinates = [] | ||||
| 
 | ||||
|         for (const coordinate of coordinates) { | ||||
|             /* The 'PointReuseAction' is a bit buggy and might generate duplicate ids. | ||||
|                 We filter those here, as the CreateWayWithPointReuseAction delegates the actual creation to here. | ||||
|                 Filtering here also prevents similar bugs in other actions | ||||
|              */ | ||||
|             if(this.coordinates.length > 0 && coordinate.nodeId !== undefined && this.coordinates[this.coordinates.length - 1].nodeId === coordinate.nodeId){ | ||||
|             if ( | ||||
|                 this.coordinates.length > 0 && | ||||
|                 coordinate.nodeId !== undefined && | ||||
|                 this.coordinates[this.coordinates.length - 1].nodeId === coordinate.nodeId | ||||
|             ) { | ||||
|                 // This is a duplicate id
 | ||||
|                 console.warn("Skipping a node in createWay to avoid a duplicate node:", coordinate,"\nThe previous coordinates are: ", this.coordinates) | ||||
|                 console.warn( | ||||
|                     "Skipping a node in createWay to avoid a duplicate node:", | ||||
|                     coordinate, | ||||
|                     "\nThe previous coordinates are: ", | ||||
|                     this.coordinates | ||||
|                 ) | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             this.coordinates.push(coordinate) | ||||
|         } | ||||
| 
 | ||||
|         this.tags = tags; | ||||
|         this._options = options; | ||||
|         this.tags = tags | ||||
|         this._options = options | ||||
|     } | ||||
| 
 | ||||
|     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
| 
 | ||||
|         const newElements: ChangeDescription[] = [] | ||||
| 
 | ||||
|         const pointIds: number[] = [] | ||||
|  | @ -60,16 +70,15 @@ export default class CreateNewWayAction extends OsmCreateAction { | |||
|             const newPoint = new CreateNewNodeAction([], coordinate.lat, coordinate.lon, { | ||||
|                 allowReuseOfPreviouslyCreatedPoints: true, | ||||
|                 changeType: null, | ||||
|                 theme: this._options.theme | ||||
|                 theme: this._options.theme, | ||||
|             }) | ||||
|             newElements.push(...await newPoint.CreateChangeDescriptions(changes)) | ||||
|             newElements.push(...(await newPoint.CreateChangeDescriptions(changes))) | ||||
|             pointIds.push(newPoint.newElementIdNumber) | ||||
|         } | ||||
| 
 | ||||
|         // We have all created (or reused) all the points!
 | ||||
|         // Time to create the actual way
 | ||||
| 
 | ||||
| 
 | ||||
|         const id = changes.getNewID() | ||||
|         this.newElementIdNumber = id | ||||
|         const newWay = <ChangeDescription>{ | ||||
|  | @ -77,18 +86,16 @@ export default class CreateNewWayAction extends OsmCreateAction { | |||
|             type: "way", | ||||
|             meta: { | ||||
|                 theme: this._options.theme, | ||||
|                 changeType: "import" | ||||
|                 changeType: "import", | ||||
|             }, | ||||
|             tags: new And(this.tags).asChange({}), | ||||
|             changes: { | ||||
|                 nodes: pointIds, | ||||
|                 coordinates: this.coordinates.map(c => [c.lon, c.lat]) | ||||
|             } | ||||
|                 coordinates: this.coordinates.map((c) => [c.lon, c.lat]), | ||||
|             }, | ||||
|         } | ||||
|         newElements.push(newWay) | ||||
|         this.newElementId = "way/" + id | ||||
|         return newElements | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,20 +1,19 @@ | |||
| import {OsmCreateAction} from "./OsmChangeAction"; | ||||
| import {Tag} from "../../Tags/Tag"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import FeaturePipelineState from "../../State/FeaturePipelineState"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | ||||
| import {GeoOperations} from "../../GeoOperations"; | ||||
| import FeatureSource from "../../FeatureSource/FeatureSource"; | ||||
| import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"; | ||||
| import CreateNewNodeAction from "./CreateNewNodeAction"; | ||||
| import CreateNewWayAction from "./CreateNewWayAction"; | ||||
| 
 | ||||
| import { OsmCreateAction } from "./OsmChangeAction" | ||||
| import { Tag } from "../../Tags/Tag" | ||||
| import { Changes } from "../Changes" | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| import FeaturePipelineState from "../../State/FeaturePipelineState" | ||||
| import { BBox } from "../../BBox" | ||||
| import { TagsFilter } from "../../Tags/TagsFilter" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| import FeatureSource from "../../FeatureSource/FeatureSource" | ||||
| import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" | ||||
| import CreateNewNodeAction from "./CreateNewNodeAction" | ||||
| import CreateNewWayAction from "./CreateNewWayAction" | ||||
| 
 | ||||
| export interface MergePointConfig { | ||||
|     withinRangeOfM: number, | ||||
|     ifMatches: TagsFilter, | ||||
|     withinRangeOfM: number | ||||
|     ifMatches: TagsFilter | ||||
|     mode: "reuse_osm_point" | "move_osm_point" | ||||
| } | ||||
| 
 | ||||
|  | @ -33,12 +32,12 @@ interface CoordinateInfo { | |||
|     /** | ||||
|      * The new coordinate | ||||
|      */ | ||||
|     lngLat: [number, number], | ||||
|     lngLat: [number, number] | ||||
|     /** | ||||
|      * If set: indicates that this point is identical to an earlier point in the way and that that point should be used. | ||||
|      * This is especially needed in closed ways, where the last CoordinateInfo will have '0' as identicalTo | ||||
|      */ | ||||
|     identicalTo?: number, | ||||
|     identicalTo?: number | ||||
|     /** | ||||
|      * Information about the closebyNode which might be reused | ||||
|      */ | ||||
|  | @ -46,8 +45,8 @@ interface CoordinateInfo { | |||
|         /** | ||||
|          * Distance in meters between the target coordinate and this candidate coordinate | ||||
|          */ | ||||
|         d: number, | ||||
|         node: any, | ||||
|         d: number | ||||
|         node: any | ||||
|         config: MergePointConfig | ||||
|     }[] | ||||
| } | ||||
|  | @ -56,54 +55,55 @@ interface CoordinateInfo { | |||
|  * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points | ||||
|  */ | ||||
| export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||
|     public newElementId: string = undefined; | ||||
|     public newElementId: string = undefined | ||||
|     public newElementIdNumber: number = undefined | ||||
|     private readonly _tags: Tag[]; | ||||
|     private readonly _tags: Tag[] | ||||
|     /** | ||||
|      * lngLat-coordinates | ||||
|      * @private | ||||
|      */ | ||||
|     private _coordinateInfo: CoordinateInfo[]; | ||||
|     private _state: FeaturePipelineState; | ||||
|     private _config: MergePointConfig[]; | ||||
|     private _coordinateInfo: CoordinateInfo[] | ||||
|     private _state: FeaturePipelineState | ||||
|     private _config: MergePointConfig[] | ||||
| 
 | ||||
|     constructor(tags: Tag[], | ||||
|     constructor( | ||||
|         tags: Tag[], | ||||
|         coordinates: [number, number][], | ||||
|         state: FeaturePipelineState, | ||||
|         config: MergePointConfig[] | ||||
|     ) { | ||||
|         super(null, true); | ||||
|         this._tags = tags; | ||||
|         this._state = state; | ||||
|         this._config = config; | ||||
|         super(null, true) | ||||
|         this._tags = tags | ||||
|         this._state = state | ||||
|         this._config = config | ||||
| 
 | ||||
|         // The main logic of this class: the coordinateInfo contains all the changes
 | ||||
|         this._coordinateInfo = this.CalculateClosebyNodes(coordinates); | ||||
| 
 | ||||
|         this._coordinateInfo = this.CalculateClosebyNodes(coordinates) | ||||
|     } | ||||
| 
 | ||||
|     public async getPreview(): Promise<FeatureSource> { | ||||
| 
 | ||||
|         const features = [] | ||||
|         let geometryMoved = false; | ||||
|         let geometryMoved = false | ||||
|         for (let i = 0; i < this._coordinateInfo.length; i++) { | ||||
|             const coordinateInfo = this._coordinateInfo[i]; | ||||
|             const coordinateInfo = this._coordinateInfo[i] | ||||
|             if (coordinateInfo.identicalTo !== undefined) { | ||||
|                 continue | ||||
|             } | ||||
|             if (coordinateInfo.closebyNodes === undefined || coordinateInfo.closebyNodes.length === 0) { | ||||
| 
 | ||||
|             if ( | ||||
|                 coordinateInfo.closebyNodes === undefined || | ||||
|                 coordinateInfo.closebyNodes.length === 0 | ||||
|             ) { | ||||
|                 const newPoint = { | ||||
|                     type: "Feature", | ||||
|                     properties: { | ||||
|                         "newpoint": "yes", | ||||
|                         id: "new-geometry-with-reuse-" + i | ||||
|                         newpoint: "yes", | ||||
|                         id: "new-geometry-with-reuse-" + i, | ||||
|                     }, | ||||
|                     geometry: { | ||||
|                         type: "Point", | ||||
|                         coordinates: coordinateInfo.lngLat | ||||
|                         coordinates: coordinateInfo.lngLat, | ||||
|                     }, | ||||
|                 } | ||||
|                 }; | ||||
|                 features.push(newPoint) | ||||
|                 continue | ||||
|             } | ||||
|  | @ -113,18 +113,20 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
|                 const moveDescription = { | ||||
|                     type: "Feature", | ||||
|                     properties: { | ||||
|                         "move": "yes", | ||||
|                         move: "yes", | ||||
|                         "osm-id": reusedPoint.node.properties.id, | ||||
|                         "id": "new-geometry-move-existing" + i, | ||||
|                         "distance": GeoOperations.distanceBetween(coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates) | ||||
|                         id: "new-geometry-move-existing" + i, | ||||
|                         distance: GeoOperations.distanceBetween( | ||||
|                             coordinateInfo.lngLat, | ||||
|                             reusedPoint.node.geometry.coordinates | ||||
|                         ), | ||||
|                     }, | ||||
|                     geometry: { | ||||
|                         type: "LineString", | ||||
|                         coordinates: [reusedPoint.node.geometry.coordinates, coordinateInfo.lngLat] | ||||
|                     } | ||||
|                         coordinates: [reusedPoint.node.geometry.coordinates, coordinateInfo.lngLat], | ||||
|                     }, | ||||
|                 } | ||||
|                 features.push(moveDescription) | ||||
| 
 | ||||
|             } else { | ||||
|                 // The geometry is moved, the point is reused
 | ||||
|                 geometryMoved = true | ||||
|  | @ -132,22 +134,24 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
|                 const reuseDescription = { | ||||
|                     type: "Feature", | ||||
|                     properties: { | ||||
|                         "move": "no", | ||||
|                         move: "no", | ||||
|                         "osm-id": reusedPoint.node.properties.id, | ||||
|                         "id": "new-geometry-reuse-existing" + i, | ||||
|                         "distance": GeoOperations.distanceBetween(coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates) | ||||
|                         id: "new-geometry-reuse-existing" + i, | ||||
|                         distance: GeoOperations.distanceBetween( | ||||
|                             coordinateInfo.lngLat, | ||||
|                             reusedPoint.node.geometry.coordinates | ||||
|                         ), | ||||
|                     }, | ||||
|                     geometry: { | ||||
|                         type: "LineString", | ||||
|                         coordinates: [coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates] | ||||
|                     } | ||||
|                         coordinates: [coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates], | ||||
|                     }, | ||||
|                 } | ||||
|                 features.push(reuseDescription) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (geometryMoved) { | ||||
| 
 | ||||
|             const coords: [number, number][] = [] | ||||
|             for (const info of this._coordinateInfo) { | ||||
|                 if (info.identicalTo !== undefined) { | ||||
|  | @ -166,21 +170,19 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
|                 } else { | ||||
|                     coords.push(info.lngLat) | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|             const newGeometry = { | ||||
|                 type: "Feature", | ||||
|                 properties: { | ||||
|                     "resulting-geometry": "yes", | ||||
|                     "id": "new-geometry" | ||||
|                     id: "new-geometry", | ||||
|                 }, | ||||
|                 geometry: { | ||||
|                     type: "LineString", | ||||
|                     coordinates: coords | ||||
|                 } | ||||
|                     coordinates: coords, | ||||
|                 }, | ||||
|             } | ||||
|             features.push(newGeometry) | ||||
| 
 | ||||
|         } | ||||
|         return StaticFeatureSource.fromGeojson(features) | ||||
|     } | ||||
|  | @ -188,7 +190,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
|     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
|         const theme = this._state?.layoutToUse?.id | ||||
|         const allChanges: ChangeDescription[] = [] | ||||
|         const nodeIdsToUse: { lat: number, lon: number, nodeId?: number }[] = [] | ||||
|         const nodeIdsToUse: { lat: number; lon: number; nodeId?: number }[] = [] | ||||
|         for (let i = 0; i < this._coordinateInfo.length; i++) { | ||||
|             const info = this._coordinateInfo[i] | ||||
|             const lat = info.lngLat[1] | ||||
|  | @ -202,17 +204,17 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
|                 const newNodeAction = new CreateNewNodeAction([], lat, lon, { | ||||
|                     allowReuseOfPreviouslyCreatedPoints: true, | ||||
|                     changeType: null, | ||||
|                     theme | ||||
|                     theme, | ||||
|                 }) | ||||
| 
 | ||||
|                 allChanges.push(...(await newNodeAction.CreateChangeDescriptions(changes))) | ||||
| 
 | ||||
|                 nodeIdsToUse.push({ | ||||
|                     lat, lon, | ||||
|                     nodeId: newNodeAction.newElementIdNumber | ||||
|                     lat, | ||||
|                     lon, | ||||
|                     nodeId: newNodeAction.newElementIdNumber, | ||||
|                 }) | ||||
|                 continue | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             const closestPoint = info.closebyNodes[0] | ||||
|  | @ -222,20 +224,20 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
|                     type: "node", | ||||
|                     id, | ||||
|                     changes: { | ||||
|                         lat, lon | ||||
|                         lat, | ||||
|                         lon, | ||||
|                     }, | ||||
|                     meta: { | ||||
|                         theme, | ||||
|                         changeType: null | ||||
|                     } | ||||
|                         changeType: null, | ||||
|                     }, | ||||
|                 }) | ||||
|             } | ||||
|             nodeIdsToUse.push({lat, lon, nodeId: id}) | ||||
|             nodeIdsToUse.push({ lat, lon, nodeId: id }) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, { | ||||
|             theme | ||||
|             theme, | ||||
|         }) | ||||
| 
 | ||||
|         allChanges.push(...(await newWay.CreateChangeDescriptions(changes))) | ||||
|  | @ -248,27 +250,26 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
|      * Calculates the main changes. | ||||
|      */ | ||||
|     private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] { | ||||
| 
 | ||||
|         const bbox = new BBox(coordinates) | ||||
|         const state = this._state | ||||
|         const allNodes = [].concat(...state?.featurePipeline?.GetFeaturesWithin("type_node", bbox.pad(1.2))??[]) | ||||
|         const maxDistance = Math.max(...this._config.map(c => c.withinRangeOfM)) | ||||
|         const allNodes = [].concat( | ||||
|             ...(state?.featurePipeline?.GetFeaturesWithin("type_node", bbox.pad(1.2)) ?? []) | ||||
|         ) | ||||
|         const maxDistance = Math.max(...this._config.map((c) => c.withinRangeOfM)) | ||||
| 
 | ||||
|         // Init coordianteinfo with undefined but the same length as coordinates
 | ||||
|         const coordinateInfo: { | ||||
|             lngLat: [number, number], | ||||
|             identicalTo?: number, | ||||
|             lngLat: [number, number] | ||||
|             identicalTo?: number | ||||
|             closebyNodes?: { | ||||
|                 d: number, | ||||
|                 node: any, | ||||
|                 d: number | ||||
|                 node: any | ||||
|                 config: MergePointConfig | ||||
|             }[] | ||||
|         }[] = coordinates.map(_ => undefined) | ||||
| 
 | ||||
|         }[] = coordinates.map((_) => undefined) | ||||
| 
 | ||||
|         // First loop: gather all information...
 | ||||
|         for (let i = 0; i < coordinates.length; i++) { | ||||
| 
 | ||||
|             if (coordinateInfo[i] !== undefined) { | ||||
|                 // Already seen, probably a duplicate coordinate
 | ||||
|                 continue | ||||
|  | @ -282,9 +283,9 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
|                 if (GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) { | ||||
|                     coordinateInfo[j] = { | ||||
|                         lngLat: coor, | ||||
|                         identicalTo: i | ||||
|                         identicalTo: i, | ||||
|                     } | ||||
|                     break; | ||||
|                     break | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|  | @ -292,8 +293,8 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
| 
 | ||||
|             // Lets search applicable points and determine the merge mode
 | ||||
|             const closebyNodes: { | ||||
|                 d: number, | ||||
|                 node: any, | ||||
|                 d: number | ||||
|                 node: any | ||||
|                 config: MergePointConfig | ||||
|             }[] = [] | ||||
|             for (const node of allNodes) { | ||||
|  | @ -310,7 +311,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
|                     if (!config.ifMatches.matchesProperties(node.properties)) { | ||||
|                         continue | ||||
|                     } | ||||
|                     closebyNodes.push({node, d, config}) | ||||
|                     closebyNodes.push({ node, d, config }) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|  | @ -322,18 +323,15 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
|             coordinateInfo[i] = { | ||||
|                 identicalTo: undefined, | ||||
|                 lngLat: coor, | ||||
|                 closebyNodes | ||||
|                 closebyNodes, | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         // Second loop: figure out which point moves where without creating conflicts
 | ||||
|         let conflictFree = true; | ||||
|         let conflictFree = true | ||||
|         do { | ||||
|             conflictFree = true; | ||||
|             conflictFree = true | ||||
|             for (let i = 0; i < coordinateInfo.length; i++) { | ||||
| 
 | ||||
|                 const coorInfo = coordinateInfo[i] | ||||
|                 if (coorInfo.identicalTo !== undefined) { | ||||
|                     continue | ||||
|  | @ -366,8 +364,6 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
|             } | ||||
|         } while (!conflictFree) | ||||
| 
 | ||||
| 
 | ||||
|         return coordinateInfo | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,61 +1,61 @@ | |||
| import {OsmObject} from "../OsmObject"; | ||||
| import OsmChangeAction from "./OsmChangeAction"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import ChangeTagAction from "./ChangeTagAction"; | ||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | ||||
| import {And} from "../../Tags/And"; | ||||
| import {Tag} from "../../Tags/Tag"; | ||||
| import { OsmObject } from "../OsmObject" | ||||
| import OsmChangeAction from "./OsmChangeAction" | ||||
| import { Changes } from "../Changes" | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| import ChangeTagAction from "./ChangeTagAction" | ||||
| import { TagsFilter } from "../../Tags/TagsFilter" | ||||
| import { And } from "../../Tags/And" | ||||
| import { Tag } from "../../Tags/Tag" | ||||
| 
 | ||||
| export default class DeleteAction extends OsmChangeAction { | ||||
| 
 | ||||
|     private readonly _softDeletionTags: TagsFilter; | ||||
|     private readonly _softDeletionTags: TagsFilter | ||||
|     private readonly meta: { | ||||
|         theme: string, | ||||
|         specialMotivation: string, | ||||
|         theme: string | ||||
|         specialMotivation: string | ||||
|         changeType: "deletion" | ||||
|     }; | ||||
|     private readonly _id: string; | ||||
|     private _hardDelete: boolean; | ||||
|     } | ||||
|     private readonly _id: string | ||||
|     private _hardDelete: boolean | ||||
| 
 | ||||
| 
 | ||||
|     constructor(id: string, | ||||
|     constructor( | ||||
|         id: string, | ||||
|         softDeletionTags: TagsFilter, | ||||
|         meta: { | ||||
|                     theme: string, | ||||
|             theme: string | ||||
|             specialMotivation: string | ||||
|         }, | ||||
|                 hardDelete: boolean) { | ||||
|         hardDelete: boolean | ||||
|     ) { | ||||
|         super(id, true) | ||||
|         this._id = id; | ||||
|         this._hardDelete = hardDelete; | ||||
|         this.meta = {...meta, changeType: "deletion"}; | ||||
|         this._softDeletionTags = new And([softDeletionTags, | ||||
|             new Tag("fixme", `A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})`) | ||||
|         ]); | ||||
| 
 | ||||
|         this._id = id | ||||
|         this._hardDelete = hardDelete | ||||
|         this.meta = { ...meta, changeType: "deletion" } | ||||
|         this._softDeletionTags = new And([ | ||||
|             softDeletionTags, | ||||
|             new Tag( | ||||
|                 "fixme", | ||||
|                 `A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})` | ||||
|             ), | ||||
|         ]) | ||||
|     } | ||||
| 
 | ||||
|     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
| 
 | ||||
|         const osmObject = await OsmObject.DownloadObjectAsync(this._id) | ||||
| 
 | ||||
|         if (this._hardDelete) { | ||||
|             return [{ | ||||
|             return [ | ||||
|                 { | ||||
|                     meta: this.meta, | ||||
|                     doDelete: true, | ||||
|                     type: osmObject.type, | ||||
|                     id: osmObject.id, | ||||
|             }] | ||||
|                 }, | ||||
|             ] | ||||
|         } else { | ||||
|             return await new ChangeTagAction( | ||||
|                 this._id, this._softDeletionTags, osmObject.tags, | ||||
|                 { | ||||
|             return await new ChangeTagAction(this._id, this._softDeletionTags, osmObject.tags, { | ||||
|                 ...this.meta, | ||||
|                     changeType: "soft-delete" | ||||
|                 } | ||||
|             ).CreateChangeDescriptions(changes) | ||||
|                 changeType: "soft-delete", | ||||
|             }).CreateChangeDescriptions(changes) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -2,22 +2,21 @@ | |||
|  * An action is a change to the OSM-database | ||||
|  * It will generate some new/modified/deleted objects, which are all bundled by the 'changes'-object | ||||
|  */ | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import { Changes } from "../Changes" | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| 
 | ||||
| export default abstract class OsmChangeAction { | ||||
| 
 | ||||
|     public readonly trackStatistics: boolean; | ||||
|     public readonly trackStatistics: boolean | ||||
|     /** | ||||
|      * The ID of the object that is the center of this change. | ||||
|      * Null if the action creates a new object (at initialization) | ||||
|      * Undefined if such an id does not make sense | ||||
|      */ | ||||
|     public readonly mainObjectId: string; | ||||
|     public readonly mainObjectId: string | ||||
|     private isUsed = false | ||||
| 
 | ||||
|     constructor(mainObjectId: string, trackStatistics: boolean = true) { | ||||
|         this.trackStatistics = trackStatistics; | ||||
|         this.trackStatistics = trackStatistics | ||||
|         this.mainObjectId = mainObjectId | ||||
|     } | ||||
| 
 | ||||
|  | @ -25,7 +24,7 @@ export default abstract class OsmChangeAction { | |||
|         if (this.isUsed) { | ||||
|             throw "This ChangeAction is already used" | ||||
|         } | ||||
|         this.isUsed = true; | ||||
|         this.isUsed = true | ||||
|         return this.CreateChangeDescriptions(changes) | ||||
|     } | ||||
| 
 | ||||
|  | @ -33,8 +32,6 @@ export default abstract class OsmChangeAction { | |||
| } | ||||
| 
 | ||||
| export abstract class OsmCreateAction extends OsmChangeAction { | ||||
| 
 | ||||
|     public newElementId: string | ||||
|     public newElementIdNumber: number | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,24 +1,24 @@ | |||
| import OsmChangeAction from "./OsmChangeAction"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import {OsmObject, OsmRelation, OsmWay} from "../OsmObject"; | ||||
| import OsmChangeAction from "./OsmChangeAction" | ||||
| import { Changes } from "../Changes" | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| import { OsmObject, OsmRelation, OsmWay } from "../OsmObject" | ||||
| 
 | ||||
| export interface RelationSplitInput { | ||||
|     relation: OsmRelation, | ||||
|     originalWayId: number, | ||||
|     allWayIdsInOrder: number[], | ||||
|     originalNodes: number[], | ||||
|     relation: OsmRelation | ||||
|     originalWayId: number | ||||
|     allWayIdsInOrder: number[] | ||||
|     originalNodes: number[] | ||||
|     allWaysNodesInOrder: number[][] | ||||
| } | ||||
| 
 | ||||
| abstract class AbstractRelationSplitHandler extends OsmChangeAction { | ||||
|     protected readonly _input: RelationSplitInput; | ||||
|     protected readonly _theme: string; | ||||
|     protected readonly _input: RelationSplitInput | ||||
|     protected readonly _theme: string | ||||
| 
 | ||||
|     constructor(input: RelationSplitInput, theme: string) { | ||||
|         super("relation/" + input.relation.id, false) | ||||
|         this._input = input; | ||||
|         this._theme = theme; | ||||
|         this._input = input | ||||
|         this._theme = theme | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -44,7 +44,7 @@ abstract class AbstractRelationSplitHandler extends OsmChangeAction { | |||
|         if (member.type === "relation") { | ||||
|             return undefined | ||||
|         } | ||||
|         return undefined; | ||||
|         return undefined | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -52,7 +52,6 @@ abstract class AbstractRelationSplitHandler extends OsmChangeAction { | |||
|  * When a way is split and this way is part of a relation, the relation should be updated too to have the new segment if relevant. | ||||
|  */ | ||||
| export default class RelationSplitHandler extends AbstractRelationSplitHandler { | ||||
| 
 | ||||
|     constructor(input: RelationSplitInput, theme: string) { | ||||
|         super(input, theme) | ||||
|     } | ||||
|  | @ -60,38 +59,43 @@ export default class RelationSplitHandler extends AbstractRelationSplitHandler { | |||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
|         if (this._input.relation.tags["type"] === "restriction") { | ||||
|             // This is a turn restriction
 | ||||
|             return new TurnRestrictionRSH(this._input, this._theme).CreateChangeDescriptions(changes) | ||||
|             return new TurnRestrictionRSH(this._input, this._theme).CreateChangeDescriptions( | ||||
|                 changes | ||||
|             ) | ||||
|         } | ||||
|         return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(changes) | ||||
|         return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions( | ||||
|             changes | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class TurnRestrictionRSH extends AbstractRelationSplitHandler { | ||||
| 
 | ||||
|     constructor(input: RelationSplitInput, theme: string) { | ||||
|         super(input, theme); | ||||
|         super(input, theme) | ||||
|     } | ||||
| 
 | ||||
|     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
| 
 | ||||
|         const relation = this._input.relation | ||||
|         const members = relation.members | ||||
| 
 | ||||
|         const selfMembers = members.filter(m => m.type === "way" && m.ref === this._input.originalWayId) | ||||
|         const selfMembers = members.filter( | ||||
|             (m) => m.type === "way" && m.ref === this._input.originalWayId | ||||
|         ) | ||||
| 
 | ||||
|         if (selfMembers.length > 1) { | ||||
|             console.warn("Detected a turn restriction where this way has multiple occurances. This is an error") | ||||
|             console.warn( | ||||
|                 "Detected a turn restriction where this way has multiple occurances. This is an error" | ||||
|             ) | ||||
|         } | ||||
|         const selfMember = selfMembers[0] | ||||
| 
 | ||||
|         if (selfMember.role === "via") { | ||||
|             // A via way can be replaced in place
 | ||||
|             return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(changes); | ||||
|             return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions( | ||||
|                 changes | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         // We have to keep only the way with a common point with the rest of the relation
 | ||||
|         // Let's figure out which member is neighbouring our way
 | ||||
| 
 | ||||
|  | @ -102,11 +106,12 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler { | |||
|         let commonPoint = commonStartPoint ?? commonEndPoint | ||||
| 
 | ||||
|         // Let's select the way to keep
 | ||||
|         const idToKeep: { id: number } = this._input.allWaysNodesInOrder.map((nodes, i) => ({ | ||||
|         const idToKeep: { id: number } = this._input.allWaysNodesInOrder | ||||
|             .map((nodes, i) => ({ | ||||
|                 nodes: nodes, | ||||
|             id: this._input.allWayIdsInOrder[i] | ||||
|                 id: this._input.allWayIdsInOrder[i], | ||||
|             })) | ||||
|             .filter(nodesId => { | ||||
|             .filter((nodesId) => { | ||||
|                 const nds = nodesId.nodes | ||||
|                 return nds[0] == commonPoint || nds[nds.length - 1] == commonPoint | ||||
|             })[0] | ||||
|  | @ -123,36 +128,34 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler { | |||
|         } | ||||
| 
 | ||||
|         const newMembers: { | ||||
|             ref: number, | ||||
|             type: "way" | "node" | "relation", | ||||
|             ref: number | ||||
|             type: "way" | "node" | "relation" | ||||
|             role: string | ||||
|         } [] = relation.members.map(m => { | ||||
|         }[] = relation.members.map((m) => { | ||||
|             if (m.type === "way" && m.ref === originalWayId) { | ||||
|                 return { | ||||
|                     ref: idToKeep.id, | ||||
|                     type: "way", | ||||
|                     role: m.role | ||||
|                     role: m.role, | ||||
|                 } | ||||
|             } | ||||
|             return m | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         return [ | ||||
|             { | ||||
|                 type: "relation", | ||||
|                 id: relation.id, | ||||
|                 changes: { | ||||
|                     members: newMembers | ||||
|                     members: newMembers, | ||||
|                 }, | ||||
|                 meta: { | ||||
|                     theme: this._theme, | ||||
|                     changeType: "relation-fix:turn_restriction" | ||||
|                     changeType: "relation-fix:turn_restriction", | ||||
|                 }, | ||||
|             }, | ||||
|         ] | ||||
|     } | ||||
|             } | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -163,26 +166,24 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler { | |||
|  * Note that the feature might appear multiple times. | ||||
|  */ | ||||
| export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { | ||||
| 
 | ||||
|     constructor(input: RelationSplitInput, theme: string) { | ||||
|         super(input, theme); | ||||
|         super(input, theme) | ||||
|     } | ||||
| 
 | ||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
| 
 | ||||
|         const wayId = this._input.originalWayId | ||||
|         const relation = this._input.relation | ||||
|         const members = relation.members | ||||
|         const originalNodes = this._input.originalNodes; | ||||
|         const originalNodes = this._input.originalNodes | ||||
|         const firstNode = originalNodes[0] | ||||
|         const lastNode = originalNodes[originalNodes.length - 1] | ||||
|         const newMembers: { type: "node" | "way" | "relation", ref: number, role: string }[] = [] | ||||
|         const newMembers: { type: "node" | "way" | "relation"; ref: number; role: string }[] = [] | ||||
| 
 | ||||
|         for (let i = 0; i < members.length; i++) { | ||||
|             const member = members[i]; | ||||
|             const member = members[i] | ||||
|             if (member.type !== "way" || member.ref !== wayId) { | ||||
|                 newMembers.push(member) | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             const nodeIdBefore = await this.targetNodeAt(i - 1, false) | ||||
|  | @ -197,10 +198,10 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { | |||
|                     newMembers.push({ | ||||
|                         ref: wId, | ||||
|                         type: "way", | ||||
|                         role: member.role | ||||
|                         role: member.role, | ||||
|                     }) | ||||
|                 } | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             const firstNodeMatchesRev = nodeIdBefore === undefined || nodeIdBefore === lastNode | ||||
|  | @ -209,14 +210,14 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { | |||
|                 // We (probably) have a reversed situation, backward situation
 | ||||
|                 for (let i1 = this._input.allWayIdsInOrder.length - 1; i1 >= 0; i1--) { | ||||
|                     // Iterate BACKWARDS
 | ||||
|                     const wId = this._input.allWayIdsInOrder[i1]; | ||||
|                     const wId = this._input.allWayIdsInOrder[i1] | ||||
|                     newMembers.push({ | ||||
|                         ref: wId, | ||||
|                         type: "way", | ||||
|                         role: member.role | ||||
|                         role: member.role, | ||||
|                     }) | ||||
|                 } | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             // Euhm, allright... Something weird is going on, but let's not care too much
 | ||||
|  | @ -225,21 +226,21 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { | |||
|                 newMembers.push({ | ||||
|                     ref: wId, | ||||
|                     type: "way", | ||||
|                     role: member.role | ||||
|                     role: member.role, | ||||
|                 }) | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         return [{ | ||||
|         return [ | ||||
|             { | ||||
|                 id: relation.id, | ||||
|                 type: "relation", | ||||
|             changes: {members: newMembers}, | ||||
|                 changes: { members: newMembers }, | ||||
|                 meta: { | ||||
|                     changeType: "relation-fix", | ||||
|                 theme: this._theme | ||||
|                     theme: this._theme, | ||||
|                 }, | ||||
|             }, | ||||
|         ] | ||||
|     } | ||||
|         }]; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,59 +1,59 @@ | |||
| import OsmChangeAction from "./OsmChangeAction"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import {Tag} from "../../Tags/Tag"; | ||||
| import FeatureSource from "../../FeatureSource/FeatureSource"; | ||||
| import {OsmNode, OsmObject, OsmWay} from "../OsmObject"; | ||||
| import {GeoOperations} from "../../GeoOperations"; | ||||
| import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"; | ||||
| import CreateNewNodeAction from "./CreateNewNodeAction"; | ||||
| import ChangeTagAction from "./ChangeTagAction"; | ||||
| import {And} from "../../Tags/And"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {OsmConnection} from "../OsmConnection"; | ||||
| import {Feature} from "@turf/turf"; | ||||
| import FeaturePipeline from "../../FeatureSource/FeaturePipeline"; | ||||
| import OsmChangeAction from "./OsmChangeAction" | ||||
| import { Changes } from "../Changes" | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| import { Tag } from "../../Tags/Tag" | ||||
| import FeatureSource from "../../FeatureSource/FeatureSource" | ||||
| import { OsmNode, OsmObject, OsmWay } from "../OsmObject" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" | ||||
| import CreateNewNodeAction from "./CreateNewNodeAction" | ||||
| import ChangeTagAction from "./ChangeTagAction" | ||||
| import { And } from "../../Tags/And" | ||||
| import { Utils } from "../../../Utils" | ||||
| import { OsmConnection } from "../OsmConnection" | ||||
| import { Feature } from "@turf/turf" | ||||
| import FeaturePipeline from "../../FeatureSource/FeaturePipeline" | ||||
| 
 | ||||
| export default class ReplaceGeometryAction extends OsmChangeAction { | ||||
|     /** | ||||
|      * The target feature - mostly used for the metadata | ||||
|      */ | ||||
|     private readonly feature: any; | ||||
|     private readonly feature: any | ||||
|     private readonly state: { | ||||
|         osmConnection: OsmConnection, | ||||
|         osmConnection: OsmConnection | ||||
|         featurePipeline: FeaturePipeline | ||||
|     }; | ||||
|     private readonly wayToReplaceId: string; | ||||
|     private readonly theme: string; | ||||
|     } | ||||
|     private readonly wayToReplaceId: string | ||||
|     private readonly theme: string | ||||
|     /** | ||||
|      * The target coordinates that should end up in OpenStreetMap. | ||||
|      * This is identical to either this.feature.geometry.coordinates or -in case of a polygon- feature.geometry.coordinates[0] | ||||
|      * Format: [lon, lat] | ||||
|      */ | ||||
|     private readonly targetCoordinates: [number, number][]; | ||||
|     private readonly targetCoordinates: [number, number][] | ||||
|     /** | ||||
|      * If a target coordinate is close to another target coordinate, 'identicalTo' will point to the first index. | ||||
|      */ | ||||
|     private readonly identicalTo: number[] | ||||
|     private readonly newTags: Tag[] | undefined; | ||||
|     private readonly newTags: Tag[] | undefined | ||||
| 
 | ||||
|     constructor( | ||||
|         state: { | ||||
|             osmConnection: OsmConnection, | ||||
|             osmConnection: OsmConnection | ||||
|             featurePipeline: FeaturePipeline | ||||
|         }, | ||||
|         feature: any, | ||||
|         wayToReplaceId: string, | ||||
|         options: { | ||||
|             theme: string, | ||||
|             theme: string | ||||
|             newTags?: Tag[] | ||||
|         } | ||||
|     ) { | ||||
|         super(wayToReplaceId, false); | ||||
|         this.state = state; | ||||
|         this.feature = feature; | ||||
|         this.wayToReplaceId = wayToReplaceId; | ||||
|         this.theme = options.theme; | ||||
|         super(wayToReplaceId, false) | ||||
|         this.state = state | ||||
|         this.feature = feature | ||||
|         this.wayToReplaceId = wayToReplaceId | ||||
|         this.theme = options.theme | ||||
| 
 | ||||
|         const geom = this.feature.geometry | ||||
|         let coordinates: [number, number][] | ||||
|  | @ -64,7 +64,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|         } | ||||
|         this.targetCoordinates = coordinates | ||||
| 
 | ||||
|         this.identicalTo = coordinates.map(_ => undefined) | ||||
|         this.identicalTo = coordinates.map((_) => undefined) | ||||
| 
 | ||||
|         for (let i = 0; i < coordinates.length; i++) { | ||||
|             if (this.identicalTo[i] !== undefined) { | ||||
|  | @ -82,7 +82,8 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
| 
 | ||||
|     // noinspection JSUnusedGlobalSymbols
 | ||||
|     public async getPreview(): Promise<FeatureSource> { | ||||
|         const {closestIds, allNodesById, detachedNodes, reprojectedNodes} = await this.GetClosestIds(); | ||||
|         const { closestIds, allNodesById, detachedNodes, reprojectedNodes } = | ||||
|             await this.GetClosestIds() | ||||
|         const preview: Feature[] = closestIds.map((newId, i) => { | ||||
|             if (this.identicalTo[i] !== undefined) { | ||||
|                 return undefined | ||||
|  | @ -92,75 +93,73 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|                 return { | ||||
|                     type: "Feature", | ||||
|                     properties: { | ||||
|                         "newpoint": "yes", | ||||
|                         "id": "replace-geometry-move-" + i, | ||||
|                         newpoint: "yes", | ||||
|                         id: "replace-geometry-move-" + i, | ||||
|                     }, | ||||
|                     geometry: { | ||||
|                         type: "Point", | ||||
|                         coordinates: this.targetCoordinates[i] | ||||
|                         coordinates: this.targetCoordinates[i], | ||||
|                     }, | ||||
|                 } | ||||
|                 }; | ||||
|             } | ||||
| 
 | ||||
|             const origNode = allNodesById.get(newId); | ||||
|             const origNode = allNodesById.get(newId) | ||||
|             return { | ||||
|                 type: "Feature", | ||||
|                 properties: { | ||||
|                     "move": "yes", | ||||
|                     move: "yes", | ||||
|                     "osm-id": newId, | ||||
|                     "id": "replace-geometry-move-" + i, | ||||
|                     "original-node-tags": JSON.stringify(origNode.tags) | ||||
|                     id: "replace-geometry-move-" + i, | ||||
|                     "original-node-tags": JSON.stringify(origNode.tags), | ||||
|                 }, | ||||
|                 geometry: { | ||||
|                     type: "LineString", | ||||
|                     coordinates: [[origNode.lon, origNode.lat], this.targetCoordinates[i]] | ||||
|                     coordinates: [[origNode.lon, origNode.lat], this.targetCoordinates[i]], | ||||
|                 }, | ||||
|             } | ||||
|             }; | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         reprojectedNodes.forEach(({newLat, newLon, nodeId}) => { | ||||
| 
 | ||||
|             const origNode = allNodesById.get(nodeId); | ||||
|             const feature : Feature =  { | ||||
|         reprojectedNodes.forEach(({ newLat, newLon, nodeId }) => { | ||||
|             const origNode = allNodesById.get(nodeId) | ||||
|             const feature: Feature = { | ||||
|                 type: "Feature", | ||||
|                 properties: { | ||||
|                     "move": "yes", | ||||
|                     "reprojection": "yes", | ||||
|                     move: "yes", | ||||
|                     reprojection: "yes", | ||||
|                     "osm-id": nodeId, | ||||
|                     "id": "replace-geometry-reproject-" + nodeId, | ||||
|                     "original-node-tags": JSON.stringify(origNode.tags) | ||||
|                     id: "replace-geometry-reproject-" + nodeId, | ||||
|                     "original-node-tags": JSON.stringify(origNode.tags), | ||||
|                 }, | ||||
|                 geometry: { | ||||
|                     type: "LineString", | ||||
|                     coordinates: [[origNode.lon, origNode.lat], [newLon, newLat]] | ||||
|                     coordinates: [ | ||||
|                         [origNode.lon, origNode.lat], | ||||
|                         [newLon, newLat], | ||||
|                     ], | ||||
|                 }, | ||||
|             } | ||||
|             }; | ||||
|             preview.push(feature) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         detachedNodes.forEach(({reason}, id) => { | ||||
|             const origNode = allNodesById.get(id); | ||||
|             const feature : Feature = { | ||||
|         detachedNodes.forEach(({ reason }, id) => { | ||||
|             const origNode = allNodesById.get(id) | ||||
|             const feature: Feature = { | ||||
|                 type: "Feature", | ||||
|                 properties: { | ||||
|                     "detach": "yes", | ||||
|                     "id": "replace-geometry-detach-" + id, | ||||
|                     detach: "yes", | ||||
|                     id: "replace-geometry-detach-" + id, | ||||
|                     "detach-reason": reason, | ||||
|                     "original-node-tags": JSON.stringify(origNode.tags) | ||||
|                     "original-node-tags": JSON.stringify(origNode.tags), | ||||
|                 }, | ||||
|                 geometry: { | ||||
|                     type: "Point", | ||||
|                     coordinates: [origNode.lon, origNode.lat] | ||||
|                     coordinates: [origNode.lon, origNode.lat], | ||||
|                 }, | ||||
|             } | ||||
|             }; | ||||
|             preview.push(feature) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         return StaticFeatureSource.fromGeojson(Utils.NoNull(preview)) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -170,45 +169,52 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|      * | ||||
|      */ | ||||
|     public async GetClosestIds(): Promise<{ | ||||
| 
 | ||||
|         // A list of the same length as targetCoordinates, containing which OSM-point to move. If undefined, a new point will be created
 | ||||
|         closestIds: number[], | ||||
|         allNodesById: Map<number, OsmNode>, | ||||
|         osmWay: OsmWay, | ||||
|         detachedNodes: Map<number, { | ||||
|             reason: string, | ||||
|         closestIds: number[] | ||||
|         allNodesById: Map<number, OsmNode> | ||||
|         osmWay: OsmWay | ||||
|         detachedNodes: Map< | ||||
|             number, | ||||
|             { | ||||
|                 reason: string | ||||
|                 hasTags: boolean | ||||
|         }>, | ||||
|         reprojectedNodes: Map<number, { | ||||
|             } | ||||
|         > | ||||
|         reprojectedNodes: Map< | ||||
|             number, | ||||
|             { | ||||
|                 /*Move the node with this ID into the way as extra node, as it has some relation with the original object*/ | ||||
|             projectAfterIndex: number, | ||||
|             distance: number, | ||||
|             newLat: number, | ||||
|             newLon: number, | ||||
|                 projectAfterIndex: number | ||||
|                 distance: number | ||||
|                 newLat: number | ||||
|                 newLon: number | ||||
|                 nodeId: number | ||||
|         }> | ||||
|             } | ||||
|         > | ||||
|     }> { | ||||
|         // TODO FIXME: if a new point has to be created, snap to already existing ways
 | ||||
| 
 | ||||
|         const nodeDb = this.state.featurePipeline.fullNodeDatabase; | ||||
|         const nodeDb = this.state.featurePipeline.fullNodeDatabase | ||||
|         if (nodeDb === undefined) { | ||||
|             throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" | ||||
|         } | ||||
|         const self = this; | ||||
|         let parsed: OsmObject[]; | ||||
|         const self = this | ||||
|         let parsed: OsmObject[] | ||||
|         { | ||||
|             // Gather the needed OsmObjects
 | ||||
|             const splitted = this.wayToReplaceId.split("/"); | ||||
|             const type = splitted[0]; | ||||
|             const idN = Number(splitted[1]); | ||||
|             const splitted = this.wayToReplaceId.split("/") | ||||
|             const type = splitted[0] | ||||
|             const idN = Number(splitted[1]) | ||||
|             if (idN < 0 || type !== "way") { | ||||
|                 throw "Invalid ID to conflate: " + this.wayToReplaceId | ||||
|             } | ||||
|             const url = `${this.state.osmConnection?._oauth_config?.url ?? "https://openstreetmap.org"}/api/0.6/${this.wayToReplaceId}/full`; | ||||
|             const url = `${ | ||||
|                 this.state.osmConnection?._oauth_config?.url ?? "https://openstreetmap.org" | ||||
|             }/api/0.6/${this.wayToReplaceId}/full` | ||||
|             const rawData = await Utils.downloadJsonCached(url, 1000) | ||||
|             parsed = OsmObject.ParseObjects(rawData.elements); | ||||
|             parsed = OsmObject.ParseObjects(rawData.elements) | ||||
|         } | ||||
|         const allNodes = parsed.filter(o => o.type === "node") | ||||
|         const allNodes = parsed.filter((o) => o.type === "node") | ||||
|         const osmWay = <OsmWay>parsed[parsed.length - 1] | ||||
|         if (osmWay.type !== "way") { | ||||
|             throw "WEIRD: expected an OSM-way as last element here!" | ||||
|  | @ -228,38 +234,42 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|          * | ||||
|          * The Replace-geometry action should try its best to honour these. Some 'wiggling' is allowed (e.g. moving an entrance a bit), but these relations should not be broken.l | ||||
|          */ | ||||
|         const distances = new Map<number /* osmId*/, | ||||
|         const distances = new Map< | ||||
|             number /* osmId*/, | ||||
|             /** target coordinate index --> distance (or undefined if a duplicate)*/ | ||||
|             number[]>(); | ||||
|             number[] | ||||
|         >() | ||||
| 
 | ||||
|         const nodeInfo = new Map<number /* osmId*/, { | ||||
|             distances: number[], | ||||
|         const nodeInfo = new Map< | ||||
|             number /* osmId*/, | ||||
|             { | ||||
|                 distances: number[] | ||||
|                 // Part of some other way then the one that should be replaced
 | ||||
|             partOfWay: boolean, | ||||
|                 partOfWay: boolean | ||||
|                 hasTags: boolean | ||||
|         }>() | ||||
|             } | ||||
|         >() | ||||
| 
 | ||||
|         for (const node of allNodes) { | ||||
| 
 | ||||
|             const parentWays = nodeDb.GetParentWays(node.id) | ||||
|             if (parentWays === undefined) { | ||||
|                 throw "PANIC: the way to replace has a node which has no parents at all. Is it deleted in the meantime?" | ||||
|             } | ||||
|             const parentWayIds = parentWays.data.map(w => w.type + "/" + w.id) | ||||
|             const parentWayIds = parentWays.data.map((w) => w.type + "/" + w.id) | ||||
|             const idIndex = parentWayIds.indexOf(this.wayToReplaceId) | ||||
|             if (idIndex < 0) { | ||||
|                 throw "PANIC: the way to replace has a node, which is _not_ part of this was according to the node..." | ||||
|             } | ||||
|             parentWayIds.splice(idIndex, 1) | ||||
|             const partOfSomeWay = parentWayIds.length > 0 | ||||
|             const hasTags = Object.keys(node.tags).length > 1; | ||||
|             const hasTags = Object.keys(node.tags).length > 1 | ||||
| 
 | ||||
|             const nodeDistances = this.targetCoordinates.map(_ => undefined) | ||||
|             const nodeDistances = this.targetCoordinates.map((_) => undefined) | ||||
|             for (let i = 0; i < this.targetCoordinates.length; i++) { | ||||
|                 if (this.identicalTo[i] !== undefined) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
|                 const targetCoordinate = this.targetCoordinates[i]; | ||||
|                 const targetCoordinate = this.targetCoordinates[i] | ||||
|                 const cp = node.centerpoint() | ||||
|                 const d = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]]) | ||||
|                 if (d > 25) { | ||||
|  | @ -268,37 +278,39 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|                 } | ||||
|                 if (d < 3 || !(hasTags || partOfSomeWay)) { | ||||
|                     // If there is some relation: cap the move distance to 3m
 | ||||
|                     nodeDistances[i] = d; | ||||
|                     nodeDistances[i] = d | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|             distances.set(node.id, nodeDistances) | ||||
|             nodeInfo.set(node.id, { | ||||
|                 distances: nodeDistances, | ||||
|                 partOfWay: partOfSomeWay, | ||||
|                 hasTags | ||||
|                 hasTags, | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         const closestIds = this.targetCoordinates.map(_ => undefined) | ||||
|         const unusedIds = new Map<number, { | ||||
|             reason: string, | ||||
|         const closestIds = this.targetCoordinates.map((_) => undefined) | ||||
|         const unusedIds = new Map< | ||||
|             number, | ||||
|             { | ||||
|                 reason: string | ||||
|                 hasTags: boolean | ||||
|         }>(); | ||||
|             } | ||||
|         >() | ||||
|         { | ||||
|             // Search best merge candidate
 | ||||
|             /** | ||||
|              * Then, we search the node that has to move the least distance and add this as mapping. | ||||
|              * We do this until no points are left | ||||
|              */ | ||||
|             let candidate: number; | ||||
|             let moveDistance: number; | ||||
|             let candidate: number | ||||
|             let moveDistance: number | ||||
|             /** | ||||
|              * The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates | ||||
|              */ | ||||
|             do { | ||||
|                 candidate = undefined; | ||||
|                 moveDistance = Infinity; | ||||
|                 candidate = undefined | ||||
|                 moveDistance = Infinity | ||||
|                 distances.forEach((distances, nodeId) => { | ||||
|                     const minDist = Math.min(...Utils.NoNull(distances)) | ||||
|                     if (moveDistance > minDist) { | ||||
|  | @ -310,14 +322,14 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
| 
 | ||||
|                 if (candidate !== undefined) { | ||||
|                     // We found a candidate... Search the corresponding target id:
 | ||||
|                     let targetId: number = undefined; | ||||
|                     let targetId: number = undefined | ||||
|                     let lowestDistance = Number.MAX_VALUE | ||||
|                     let nodeDistances = distances.get(candidate) | ||||
|                     for (let i = 0; i < nodeDistances.length; i++) { | ||||
|                         const d = nodeDistances[i] | ||||
|                         if (d !== undefined && d < lowestDistance) { | ||||
|                             lowestDistance = d; | ||||
|                             targetId = i; | ||||
|                             lowestDistance = d | ||||
|                             targetId = i | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|  | @ -330,14 +342,14 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|                         closestIds[targetId] = candidate | ||||
| 
 | ||||
|                         // To indicate that this targetCoordinate is taken, we remove them from the distances matrix
 | ||||
|                         distances.forEach(dists => { | ||||
|                         distances.forEach((dists) => { | ||||
|                             dists[targetId] = undefined | ||||
|                         }) | ||||
|                     } else { | ||||
|                         // Seems like all the targetCoordinates have found a source point
 | ||||
|                         unusedIds.set(candidate, { | ||||
|                             reason: "Unused by new way", | ||||
|                             hasTags: nodeInfo.get(candidate).hasTags | ||||
|                             hasTags: nodeInfo.get(candidate).hasTags, | ||||
|                         }) | ||||
|                     } | ||||
|                 } | ||||
|  | @ -348,18 +360,21 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|         distances.forEach((_, nodeId) => { | ||||
|             unusedIds.set(nodeId, { | ||||
|                 reason: "Unused by new way", | ||||
|                 hasTags: nodeInfo.get(nodeId).hasTags | ||||
|                 hasTags: nodeInfo.get(nodeId).hasTags, | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         const reprojectedNodes = new Map<number, { | ||||
|         const reprojectedNodes = new Map< | ||||
|             number, | ||||
|             { | ||||
|                 /*Move the node with this ID into the way as extra node, as it has some relation with the original object*/ | ||||
|             projectAfterIndex: number, | ||||
|             distance: number, | ||||
|             newLat: number, | ||||
|             newLon: number, | ||||
|                 projectAfterIndex: number | ||||
|                 distance: number | ||||
|                 newLat: number | ||||
|                 newLon: number | ||||
|                 nodeId: number | ||||
|         }>(); | ||||
|             } | ||||
|         >() | ||||
|         { | ||||
|             // Lets check the unused ids: can they be detached or do they signify some relation with the object?
 | ||||
|             unusedIds.forEach(({}, id) => { | ||||
|  | @ -379,36 +394,32 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|                     properties: {}, | ||||
|                     geometry: { | ||||
|                         type: "LineString", | ||||
|                         coordinates: self.targetCoordinates | ||||
|                         coordinates: self.targetCoordinates, | ||||
|                     }, | ||||
|                 } | ||||
|                 }; | ||||
|                 const projected = GeoOperations.nearestPoint( | ||||
|                     way, [node.lon, node.lat] | ||||
|                 ) | ||||
|                 const projected = GeoOperations.nearestPoint(way, [node.lon, node.lat]) | ||||
|                 reprojectedNodes.set(id, { | ||||
|                     newLon: projected.geometry.coordinates[0], | ||||
|                     newLat: projected.geometry.coordinates[1], | ||||
|                     projectAfterIndex: projected.properties.index, | ||||
|                     distance: projected.properties.dist, | ||||
|                     nodeId: id | ||||
|                     nodeId: id, | ||||
|                 }) | ||||
|             }) | ||||
| 
 | ||||
|             reprojectedNodes.forEach((_, nodeId) => unusedIds.delete(nodeId)) | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         return {closestIds, allNodesById, osmWay, detachedNodes: unusedIds, reprojectedNodes}; | ||||
|         return { closestIds, allNodesById, osmWay, detachedNodes: unusedIds, reprojectedNodes } | ||||
|     } | ||||
| 
 | ||||
|     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
|         const nodeDb = this.state.featurePipeline.fullNodeDatabase; | ||||
|         const nodeDb = this.state.featurePipeline.fullNodeDatabase | ||||
|         if (nodeDb === undefined) { | ||||
|             throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" | ||||
|         } | ||||
| 
 | ||||
|         const {closestIds, osmWay, detachedNodes, reprojectedNodes} = await this.GetClosestIds() | ||||
|         const { closestIds, osmWay, detachedNodes, reprojectedNodes } = await this.GetClosestIds() | ||||
|         const allChanges: ChangeDescription[] = [] | ||||
|         const actualIdsToUse: number[] = [] | ||||
|         for (let i = 0; i < closestIds.length; i++) { | ||||
|  | @ -417,47 +428,43 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|                 actualIdsToUse.push(actualIdsToUse[j]) | ||||
|                 continue | ||||
|             } | ||||
|             const closestId = closestIds[i]; | ||||
|             const closestId = closestIds[i] | ||||
|             const [lon, lat] = this.targetCoordinates[i] | ||||
|             if (closestId === undefined) { | ||||
| 
 | ||||
|                 const newNodeAction = new CreateNewNodeAction( | ||||
|                     [], | ||||
|                     lat, lon, | ||||
|                     { | ||||
|                 const newNodeAction = new CreateNewNodeAction([], lat, lon, { | ||||
|                     allowReuseOfPreviouslyCreatedPoints: true, | ||||
|                         theme: this.theme, changeType: null | ||||
|                     theme: this.theme, | ||||
|                     changeType: null, | ||||
|                 }) | ||||
|                 const changeDescr = await newNodeAction.CreateChangeDescriptions(changes) | ||||
|                 allChanges.push(...changeDescr) | ||||
|                 actualIdsToUse.push(newNodeAction.newElementIdNumber) | ||||
| 
 | ||||
|             } else { | ||||
|                 const change = <ChangeDescription>{ | ||||
|                     id: closestId, | ||||
|                     type: "node", | ||||
|                     meta: { | ||||
|                         theme: this.theme, | ||||
|                         changeType: "move" | ||||
|                         changeType: "move", | ||||
|                     }, | ||||
|                     changes: {lon, lat} | ||||
|                     changes: { lon, lat }, | ||||
|                 } | ||||
|                 actualIdsToUse.push(closestId) | ||||
|                 allChanges.push(change) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         if (this.newTags !== undefined && this.newTags.length > 0) { | ||||
|             const addExtraTags = new ChangeTagAction( | ||||
|                 this.wayToReplaceId, | ||||
|                 new And(this.newTags), | ||||
|                 osmWay.tags, { | ||||
|                 osmWay.tags, | ||||
|                 { | ||||
|                     theme: this.theme, | ||||
|                     changeType: "conflation" | ||||
|                     changeType: "conflation", | ||||
|                 } | ||||
|             ) | ||||
|             allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes)) | ||||
|             allChanges.push(...(await addExtraTags.CreateChangeDescriptions(changes))) | ||||
|         } | ||||
| 
 | ||||
|         const newCoordinates = [...this.targetCoordinates] | ||||
|  | @ -468,13 +475,11 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|             const proj = Array.from(reprojectedNodes.values()) | ||||
|             proj.sort((a, b) => { | ||||
|                 // Sort descending
 | ||||
|                 const diff = b.projectAfterIndex - a.projectAfterIndex; | ||||
|                 const diff = b.projectAfterIndex - a.projectAfterIndex | ||||
|                 if (diff !== 0) { | ||||
|                     return diff | ||||
|                 } | ||||
|                 return b.distance - a.distance; | ||||
| 
 | ||||
| 
 | ||||
|                 return b.distance - a.distance | ||||
|             }) | ||||
| 
 | ||||
|             for (const reprojectedNode of proj) { | ||||
|  | @ -483,13 +488,20 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|                     type: "node", | ||||
|                     meta: { | ||||
|                         theme: this.theme, | ||||
|                         changeType: "move" | ||||
|                         changeType: "move", | ||||
|                     }, | ||||
|                     changes: {lon: reprojectedNode.newLon, lat: reprojectedNode.newLat} | ||||
|                     changes: { lon: reprojectedNode.newLon, lat: reprojectedNode.newLat }, | ||||
|                 } | ||||
|                 allChanges.push(change) | ||||
|                 actualIdsToUse.splice(reprojectedNode.projectAfterIndex + 1, 0, reprojectedNode.nodeId) | ||||
|                 newCoordinates.splice(reprojectedNode.projectAfterIndex + 1, 0, [reprojectedNode.newLon, reprojectedNode.newLat]) | ||||
|                 actualIdsToUse.splice( | ||||
|                     reprojectedNode.projectAfterIndex + 1, | ||||
|                     0, | ||||
|                     reprojectedNode.nodeId | ||||
|                 ) | ||||
|                 newCoordinates.splice(reprojectedNode.projectAfterIndex + 1, 0, [ | ||||
|                     reprojectedNode.newLon, | ||||
|                     reprojectedNode.newLat, | ||||
|                 ]) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -499,42 +511,46 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|             id: osmWay.id, | ||||
|             changes: { | ||||
|                 nodes: actualIdsToUse, | ||||
|                 coordinates: newCoordinates | ||||
|                 coordinates: newCoordinates, | ||||
|             }, | ||||
|             meta: { | ||||
|                 theme: this.theme, | ||||
|                 changeType: "conflation" | ||||
|             } | ||||
|                 changeType: "conflation", | ||||
|             }, | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         // Some nodes might need to be deleted
 | ||||
|         if (detachedNodes.size > 0) { | ||||
|             detachedNodes.forEach(({hasTags, reason}, nodeId) => { | ||||
|             detachedNodes.forEach(({ hasTags, reason }, nodeId) => { | ||||
|                 const parentWays = nodeDb.GetParentWays(nodeId) | ||||
|                 const index = parentWays.data.map(w => w.id).indexOf(osmWay.id) | ||||
|                 const index = parentWays.data.map((w) => w.id).indexOf(osmWay.id) | ||||
|                 if (index < 0) { | ||||
|                     console.error("ReplaceGeometryAction is trying to detach node " + nodeId + ", but it isn't listed as being part of way " + osmWay.id) | ||||
|                     return; | ||||
|                     console.error( | ||||
|                         "ReplaceGeometryAction is trying to detach node " + | ||||
|                             nodeId + | ||||
|                             ", but it isn't listed as being part of way " + | ||||
|                             osmWay.id | ||||
|                     ) | ||||
|                     return | ||||
|                 } | ||||
|                 // We detachted this node - so we unregister
 | ||||
|                 parentWays.data.splice(index, 1) | ||||
|                 parentWays.ping(); | ||||
|                 parentWays.ping() | ||||
| 
 | ||||
|                 if (hasTags) { | ||||
|                     // Has tags: we leave this node alone
 | ||||
|                     return; | ||||
|                     return | ||||
|                 } | ||||
|                 if (parentWays.data.length != 0) { | ||||
|                     // Still part of other ways: we leave this node alone!
 | ||||
|                     return; | ||||
|                     return | ||||
|                 } | ||||
| 
 | ||||
|                 console.log("Removing node " + nodeId, "as it isn't needed anymore by any way") | ||||
|                 allChanges.push({ | ||||
|                     meta: { | ||||
|                         theme: this.theme, | ||||
|                         changeType: "delete" | ||||
|                         changeType: "delete", | ||||
|                     }, | ||||
|                     doDelete: true, | ||||
|                     type: "node", | ||||
|  | @ -545,6 +561,4 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
| 
 | ||||
|         return allChanges | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,21 +1,21 @@ | |||
| import {OsmObject, OsmWay} from "../OsmObject"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {GeoOperations} from "../../GeoOperations"; | ||||
| import OsmChangeAction from "./OsmChangeAction"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import RelationSplitHandler from "./RelationSplitHandler"; | ||||
| import { OsmObject, OsmWay } from "../OsmObject" | ||||
| import { Changes } from "../Changes" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| import OsmChangeAction from "./OsmChangeAction" | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| import RelationSplitHandler from "./RelationSplitHandler" | ||||
| 
 | ||||
| interface SplitInfo { | ||||
|     originalIndex?: number, // or negative for new elements
 | ||||
|     lngLat: [number, number], | ||||
|     originalIndex?: number // or negative for new elements
 | ||||
|     lngLat: [number, number] | ||||
|     doSplit: boolean | ||||
| } | ||||
| 
 | ||||
| export default class SplitAction extends OsmChangeAction { | ||||
|     private readonly wayId: string; | ||||
|     private readonly _splitPointsCoordinates: [number, number] []// lon, lat
 | ||||
|     private _meta: { theme: string, changeType: "split" }; | ||||
|     private _toleranceInMeters: number; | ||||
|     private readonly wayId: string | ||||
|     private readonly _splitPointsCoordinates: [number, number][] // lon, lat
 | ||||
|     private _meta: { theme: string; changeType: "split" } | ||||
|     private _toleranceInMeters: number | ||||
| 
 | ||||
|     /** | ||||
|      * Create a changedescription for splitting a point. | ||||
|  | @ -25,12 +25,17 @@ export default class SplitAction extends OsmChangeAction { | |||
|      * @param meta | ||||
|      * @param toleranceInMeters: if a splitpoint closer then this amount of meters to an existing point, the existing point will be used to split the line instead of a new point | ||||
|      */ | ||||
|     constructor(wayId: string, splitPointCoordinates: [number, number][], meta: { theme: string }, toleranceInMeters = 5) { | ||||
|     constructor( | ||||
|         wayId: string, | ||||
|         splitPointCoordinates: [number, number][], | ||||
|         meta: { theme: string }, | ||||
|         toleranceInMeters = 5 | ||||
|     ) { | ||||
|         super(wayId, true) | ||||
|         this.wayId = wayId; | ||||
|         this.wayId = wayId | ||||
|         this._splitPointsCoordinates = splitPointCoordinates | ||||
|         this._toleranceInMeters = toleranceInMeters; | ||||
|         this._meta = {...meta, changeType: "split"}; | ||||
|         this._toleranceInMeters = toleranceInMeters | ||||
|         this._meta = { ...meta, changeType: "split" } | ||||
|     } | ||||
| 
 | ||||
|     private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] { | ||||
|  | @ -47,12 +52,12 @@ export default class SplitAction extends OsmChangeAction { | |||
|             } | ||||
|         } | ||||
|         wayParts.push(currentPart) | ||||
|         return wayParts.filter(wp => wp.length > 0) | ||||
|         return wayParts.filter((wp) => wp.length > 0) | ||||
|     } | ||||
| 
 | ||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
|         const originalElement = <OsmWay>await OsmObject.DownloadObjectAsync(this.wayId) | ||||
|         const originalNodes = originalElement.nodes; | ||||
|         const originalNodes = originalElement.nodes | ||||
| 
 | ||||
|         // First, calculate splitpoints and remove points close to one another
 | ||||
|         const splitInfo = this.CalculateSplitCoordinates(originalElement, this._toleranceInMeters) | ||||
|  | @ -64,19 +69,19 @@ export default class SplitAction extends OsmChangeAction { | |||
|             if (element.originalIndex >= 0) { | ||||
|                 element.originalIndex = originalElement.nodes[element.originalIndex] | ||||
|             } else { | ||||
|                 element.originalIndex = changes.getNewID(); | ||||
|                 element.originalIndex = changes.getNewID() | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Next up is creating actual parts from this
 | ||||
|         const wayParts: SplitInfo[][] = SplitAction.SegmentSplitInfo(splitInfo); | ||||
|         const wayParts: SplitInfo[][] = SplitAction.SegmentSplitInfo(splitInfo) | ||||
|         // Allright! At this point, we have our new ways!
 | ||||
|         // Which one is the longest of them (and can keep the id)?
 | ||||
| 
 | ||||
|         let longest = undefined; | ||||
|         let longest = undefined | ||||
|         for (const wayPart of wayParts) { | ||||
|             if (longest === undefined) { | ||||
|                 longest = wayPart; | ||||
|                 longest = wayPart | ||||
|                 continue | ||||
|             } | ||||
|             if (wayPart.length > longest.length) { | ||||
|  | @ -88,16 +93,16 @@ export default class SplitAction extends OsmChangeAction { | |||
|         // Let's create the new points as needed
 | ||||
|         for (const element of splitInfo) { | ||||
|             if (element.originalIndex >= 0) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
|             changeDescription.push({ | ||||
|                 type: "node", | ||||
|                 id: element.originalIndex, | ||||
|                 changes: { | ||||
|                     lon: element.lngLat[0], | ||||
|                     lat: element.lngLat[1] | ||||
|                     lat: element.lngLat[1], | ||||
|                 }, | ||||
|                 meta: this._meta | ||||
|                 meta: this._meta, | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|  | @ -107,24 +112,23 @@ export default class SplitAction extends OsmChangeAction { | |||
|         const allWaysNodesInOrder: number[][] = [] | ||||
|         // Lets create OsmWays based on them
 | ||||
|         for (const wayPart of wayParts) { | ||||
| 
 | ||||
|             let isOriginal = wayPart === longest | ||||
|             if (isOriginal) { | ||||
|                 // We change the actual element!
 | ||||
|                 const nodeIds = wayPart.map(p => p.originalIndex) | ||||
|                 const nodeIds = wayPart.map((p) => p.originalIndex) | ||||
|                 changeDescription.push({ | ||||
|                     type: "way", | ||||
|                     id: originalElement.id, | ||||
|                     changes: { | ||||
|                         coordinates: wayPart.map(p => p.lngLat), | ||||
|                         nodes: nodeIds | ||||
|                         coordinates: wayPart.map((p) => p.lngLat), | ||||
|                         nodes: nodeIds, | ||||
|                     }, | ||||
|                     meta: this._meta | ||||
|                     meta: this._meta, | ||||
|                 }) | ||||
|                 allWayIdsInOrder.push(originalElement.id) | ||||
|                 allWaysNodesInOrder.push(nodeIds) | ||||
|             } else { | ||||
|                 let id = changes.getNewID(); | ||||
|                 let id = changes.getNewID() | ||||
|                 // Copy the tags from the original object onto the new
 | ||||
|                 const kv = [] | ||||
|                 for (const k in originalElement.tags) { | ||||
|  | @ -132,20 +136,20 @@ export default class SplitAction extends OsmChangeAction { | |||
|                         continue | ||||
|                     } | ||||
|                     if (k.startsWith("_") || k === "id") { | ||||
|                         continue; | ||||
|                         continue | ||||
|                     } | ||||
|                     kv.push({k: k, v: originalElement.tags[k]}) | ||||
|                     kv.push({ k: k, v: originalElement.tags[k] }) | ||||
|                 } | ||||
|                 const nodeIds = wayPart.map(p => p.originalIndex) | ||||
|                 const nodeIds = wayPart.map((p) => p.originalIndex) | ||||
|                 changeDescription.push({ | ||||
|                     type: "way", | ||||
|                     id: id, | ||||
|                     tags: kv, | ||||
|                     changes: { | ||||
|                         coordinates: wayPart.map(p => p.lngLat), | ||||
|                         nodes: nodeIds | ||||
|                         coordinates: wayPart.map((p) => p.lngLat), | ||||
|                         nodes: nodeIds, | ||||
|                     }, | ||||
|                     meta: this._meta | ||||
|                     meta: this._meta, | ||||
|                 }) | ||||
| 
 | ||||
|                 allWayIdsInOrder.push(id) | ||||
|  | @ -157,13 +161,16 @@ export default class SplitAction extends OsmChangeAction { | |||
|         // At least, the order of the ways is identical, so we can keep the same roles
 | ||||
|         const relations = await OsmObject.DownloadReferencingRelations(this.wayId) | ||||
|         for (const relation of relations) { | ||||
|             const changDescrs = await new RelationSplitHandler({ | ||||
|             const changDescrs = await new RelationSplitHandler( | ||||
|                 { | ||||
|                     relation: relation, | ||||
|                     allWayIdsInOrder: allWayIdsInOrder, | ||||
|                     originalNodes: originalNodes, | ||||
|                     allWaysNodesInOrder: allWaysNodesInOrder, | ||||
|                     originalWayId: originalElement.id, | ||||
|             }, this._meta.theme).CreateChangeDescriptions(changes) | ||||
|                 }, | ||||
|                 this._meta.theme | ||||
|             ).CreateChangeDescriptions(changes) | ||||
|             changeDescription.push(...changDescrs) | ||||
|         } | ||||
| 
 | ||||
|  | @ -180,15 +187,15 @@ export default class SplitAction extends OsmChangeAction { | |||
|     private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] { | ||||
|         const wayGeoJson = osmWay.asGeoJson() | ||||
|         // Should be [lon, lat][]
 | ||||
|         const originalPoints: [number, number][] = osmWay.coordinates.map(c => [c[1], c[0]]) | ||||
|         const originalPoints: [number, number][] = osmWay.coordinates.map((c) => [c[1], c[0]]) | ||||
|         const allPoints: { | ||||
|             // lon, lat
 | ||||
|             coordinates: [number, number], | ||||
|             isSplitPoint: boolean, | ||||
|             originalIndex?: number, // Original index
 | ||||
|             dist: number, // Distance from the nearest point on the original line
 | ||||
|             coordinates: [number, number] | ||||
|             isSplitPoint: boolean | ||||
|             originalIndex?: number // Original index
 | ||||
|             dist: number // Distance from the nearest point on the original line
 | ||||
|             location: number // Distance from the start of the way
 | ||||
|         }[] = this._splitPointsCoordinates.map(c => { | ||||
|         }[] = this._splitPointsCoordinates.map((c) => { | ||||
|             // From the turf.js docs:
 | ||||
|             // The properties object will contain three values:
 | ||||
|             // - `index`: closest point was found on nth line part,
 | ||||
|  | @ -196,32 +203,31 @@ export default class SplitAction extends OsmChangeAction { | |||
|             // `location`: distance along the line between start and the closest point.
 | ||||
|             let projected = GeoOperations.nearestPoint(wayGeoJson, c) | ||||
|             // c is lon lat
 | ||||
|             return ({ | ||||
|             return { | ||||
|                 coordinates: c, | ||||
|                 isSplitPoint: true, | ||||
|                 dist: projected.properties.dist, | ||||
|                 location: projected.properties.location | ||||
|             }); | ||||
|                 location: projected.properties.location, | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         // We have a bunch of coordinates here: [ [lon, lon], [lat, lon], ...] ...
 | ||||
|         // We project them onto the line (which should yield pretty much the same point and add them to allPoints
 | ||||
|         for (let i = 0; i < originalPoints.length; i++) { | ||||
|             let originalPoint = originalPoints[i]; | ||||
|             let originalPoint = originalPoints[i] | ||||
|             let projected = GeoOperations.nearestPoint(wayGeoJson, originalPoint) | ||||
|             allPoints.push({ | ||||
|                 coordinates: originalPoint, | ||||
|                 isSplitPoint: false, | ||||
|                 location: projected.properties.location, | ||||
|                 originalIndex: i, | ||||
|                 dist: projected.properties.dist | ||||
|                 dist: projected.properties.dist, | ||||
|             }) | ||||
|         } | ||||
|         // At this point, we have a list of both the split point and the old points, with some properties to discriminate between them
 | ||||
|         // We sort this list so that the new points are at the same location
 | ||||
|         allPoints.sort((a, b) => a.location - b.location) | ||||
| 
 | ||||
| 
 | ||||
|         for (let i = allPoints.length - 2; i >= 1; i--) { | ||||
|             // We 'merge' points with already existing nodes if they are close enough to avoid closeby elements
 | ||||
| 
 | ||||
|  | @ -244,7 +250,7 @@ export default class SplitAction extends OsmChangeAction { | |||
| 
 | ||||
|             if (distToNext * 1000 > toleranceInM && distToPrev * 1000 > toleranceInM) { | ||||
|                 // Both are too far away to mark them as the split point
 | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             let closest = nextPoint | ||||
|  | @ -256,9 +262,8 @@ export default class SplitAction extends OsmChangeAction { | |||
|                 // We can not split on the first or last points...
 | ||||
|                 continue | ||||
|             } | ||||
|             closest.isSplitPoint = true; | ||||
|             closest.isSplitPoint = true | ||||
|             allPoints.splice(i, 1) | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         const splitInfo: SplitInfo[] = [] | ||||
|  | @ -267,19 +272,17 @@ export default class SplitAction extends OsmChangeAction { | |||
|         for (const p of allPoints) { | ||||
|             let index = p.originalIndex | ||||
|             if (index === undefined) { | ||||
|                 index = nextId; | ||||
|                 nextId--; | ||||
|                 index = nextId | ||||
|                 nextId-- | ||||
|             } | ||||
|             const splitInfoElement = { | ||||
|                 originalIndex: index, | ||||
|                 lngLat: p.coordinates, | ||||
|                 doSplit: p.isSplitPoint | ||||
|                 doSplit: p.isSplitPoint, | ||||
|             } | ||||
|             splitInfo.push(splitInfoElement) | ||||
|         } | ||||
| 
 | ||||
|         return splitInfo | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,106 +1,110 @@ | |||
| import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import OsmChangeAction from "./Actions/OsmChangeAction"; | ||||
| import {ChangeDescription, ChangeDescriptionTools} from "./Actions/ChangeDescription"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| import SimpleMetaTagger from "../SimpleMetaTagger"; | ||||
| import CreateNewNodeAction from "./Actions/CreateNewNodeAction"; | ||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | ||||
| import {ElementStorage} from "../ElementStorage"; | ||||
| import {GeoLocationPointProperties} from "../Actors/GeoLocationHandler"; | ||||
| import {GeoOperations} from "../GeoOperations"; | ||||
| import {ChangesetHandler, ChangesetTag} from "./ChangesetHandler"; | ||||
| import {OsmConnection} from "./OsmConnection"; | ||||
| import { OsmNode, OsmObject, OsmRelation, OsmWay } from "./OsmObject" | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import Constants from "../../Models/Constants" | ||||
| import OsmChangeAction from "./Actions/OsmChangeAction" | ||||
| import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescription" | ||||
| import { Utils } from "../../Utils" | ||||
| import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||
| import SimpleMetaTagger from "../SimpleMetaTagger" | ||||
| import CreateNewNodeAction from "./Actions/CreateNewNodeAction" | ||||
| import FeatureSource from "../FeatureSource/FeatureSource" | ||||
| import { ElementStorage } from "../ElementStorage" | ||||
| import { GeoLocationPointProperties } from "../Actors/GeoLocationHandler" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler" | ||||
| import { OsmConnection } from "./OsmConnection" | ||||
| 
 | ||||
| /** | ||||
|  * Handles all changes made to OSM. | ||||
|  * Needs an authenticator via OsmConnection | ||||
|  */ | ||||
| export class Changes { | ||||
| 
 | ||||
|     public readonly name = "Newly added features" | ||||
|     /** | ||||
|      * All the newly created features as featureSource + all the modified features | ||||
|      */ | ||||
|     public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]); | ||||
|     public readonly pendingChanges: UIEventSource<ChangeDescription[]> = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", []) | ||||
|     public features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||
|     public readonly pendingChanges: UIEventSource<ChangeDescription[]> = | ||||
|         LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", []) | ||||
|     public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined) | ||||
|     public readonly state: { allElements: ElementStorage; osmConnection: OsmConnection } | ||||
|     public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined) | ||||
| 
 | ||||
|     private historicalUserLocations: FeatureSource | ||||
|     private _nextId: number = -1; // Newly assigned ID's are negative
 | ||||
|     private readonly isUploading = new UIEventSource(false); | ||||
|     private _nextId: number = -1 // Newly assigned ID's are negative
 | ||||
|     private readonly isUploading = new UIEventSource(false) | ||||
|     private readonly previouslyCreated: OsmObject[] = [] | ||||
|     private readonly _leftRightSensitive: boolean; | ||||
|     private _changesetHandler: ChangesetHandler; | ||||
|     private readonly _leftRightSensitive: boolean | ||||
|     private _changesetHandler: ChangesetHandler | ||||
| 
 | ||||
|     constructor( | ||||
|         state?: { | ||||
|             allElements: ElementStorage, | ||||
|             allElements: ElementStorage | ||||
|             osmConnection: OsmConnection | ||||
|         }, | ||||
|         leftRightSensitive: boolean = false) { | ||||
|         this._leftRightSensitive = leftRightSensitive; | ||||
|         leftRightSensitive: boolean = false | ||||
|     ) { | ||||
|         this._leftRightSensitive = leftRightSensitive | ||||
|         // We keep track of all changes just as well
 | ||||
|         this.allChanges.setData([...this.pendingChanges.data]) | ||||
|         // If a pending change contains a negative ID, we save that
 | ||||
|         this._nextId = Math.min(-1, ...this.pendingChanges.data?.map(pch => pch.id) ?? []) | ||||
|         this.state = state; | ||||
|         this._changesetHandler = state?.osmConnection?.CreateChangesetHandler(state.allElements, this) | ||||
|         this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id) ?? [])) | ||||
|         this.state = state | ||||
|         this._changesetHandler = state?.osmConnection?.CreateChangesetHandler( | ||||
|             state.allElements, | ||||
|             this | ||||
|         ) | ||||
| 
 | ||||
|         // Note: a changeset might be reused which was opened just before and might have already used some ids
 | ||||
|         // This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
 | ||||
|     } | ||||
| 
 | ||||
|     static createChangesetFor(csId: string, | ||||
|     static createChangesetFor( | ||||
|         csId: string, | ||||
|         allChanges: { | ||||
|                                           modifiedObjects: OsmObject[], | ||||
|                                           newObjects: OsmObject[], | ||||
|             modifiedObjects: OsmObject[] | ||||
|             newObjects: OsmObject[] | ||||
|             deletedObjects: OsmObject[] | ||||
|                                       }): string { | ||||
| 
 | ||||
|         } | ||||
|     ): string { | ||||
|         const changedElements = allChanges.modifiedObjects ?? [] | ||||
|         const newElements = allChanges.newObjects ?? [] | ||||
|         const deletedElements = allChanges.deletedObjects ?? [] | ||||
| 
 | ||||
|         let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`; | ||||
|         let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>` | ||||
|         if (newElements.length > 0) { | ||||
|             changes += | ||||
|                 "\n<create>\n" + | ||||
|                 newElements.map(e => e.ChangesetXML(csId)).join("\n") + | ||||
|                 "</create>"; | ||||
|                 newElements.map((e) => e.ChangesetXML(csId)).join("\n") + | ||||
|                 "</create>" | ||||
|         } | ||||
|         if (changedElements.length > 0) { | ||||
|             changes += | ||||
|                 "\n<modify>\n" + | ||||
|                 changedElements.map(e => e.ChangesetXML(csId)).join("\n") + | ||||
|                 "\n</modify>"; | ||||
|                 changedElements.map((e) => e.ChangesetXML(csId)).join("\n") + | ||||
|                 "\n</modify>" | ||||
|         } | ||||
| 
 | ||||
|         if (deletedElements.length > 0) { | ||||
|             changes += | ||||
|                 "\n<delete>\n" + | ||||
|                 deletedElements.map(e => e.ChangesetXML(csId)).join("\n") + | ||||
|                 deletedElements.map((e) => e.ChangesetXML(csId)).join("\n") + | ||||
|                 "\n</delete>" | ||||
|         } | ||||
| 
 | ||||
|         changes += "</osmChange>"; | ||||
|         return changes; | ||||
|         changes += "</osmChange>" | ||||
|         return changes | ||||
|     } | ||||
| 
 | ||||
|     private static GetNeededIds(changes: ChangeDescription[]) { | ||||
|         return Utils.Dedup(changes.filter(c => c.id >= 0) | ||||
|             .map(c => c.type + "/" + c.id)) | ||||
|         return Utils.Dedup(changes.filter((c) => c.id >= 0).map((c) => c.type + "/" + c.id)) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a new ID and updates the value for the next ID | ||||
|      */ | ||||
|     public getNewID() { | ||||
|         return this._nextId--; | ||||
|         return this._nextId-- | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -109,64 +113,71 @@ export class Changes { | |||
|      */ | ||||
|     public async flushChanges(flushreason: string = undefined): Promise<void> { | ||||
|         if (this.pendingChanges.data.length === 0) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         if (this.isUploading.data) { | ||||
|             console.log("Is already uploading... Abort") | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         console.log("Uploading changes due to: ", flushreason) | ||||
|         this.isUploading.setData(true) | ||||
|         try { | ||||
|             const csNumber = await this.flushChangesAsync() | ||||
|             this.isUploading.setData(false) | ||||
|             console.log("Changes flushed. Your changeset is " + csNumber); | ||||
|             console.log("Changes flushed. Your changeset is " + csNumber) | ||||
|         } catch (e) { | ||||
|             this.isUploading.setData(false) | ||||
|             console.error("Flushing changes failed due to", e); | ||||
|             console.error("Flushing changes failed due to", e) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public async applyAction(action: OsmChangeAction): Promise<void> { | ||||
|         const changeDescriptions = await action.Perform(this) | ||||
|         changeDescriptions[0].meta.distanceToObject = this.calculateDistanceToChanges(action, changeDescriptions) | ||||
|         changeDescriptions[0].meta.distanceToObject = this.calculateDistanceToChanges( | ||||
|             action, | ||||
|             changeDescriptions | ||||
|         ) | ||||
|         this.applyChanges(changeDescriptions) | ||||
|     } | ||||
| 
 | ||||
|     public applyChanges(changes: ChangeDescription[]) { | ||||
|         console.log("Received changes:", changes) | ||||
|         this.pendingChanges.data.push(...changes); | ||||
|         this.pendingChanges.ping(); | ||||
|         this.pendingChanges.data.push(...changes) | ||||
|         this.pendingChanges.ping() | ||||
|         this.allChanges.data.push(...changes) | ||||
|         this.allChanges.ping() | ||||
|     } | ||||
| 
 | ||||
|     private calculateDistanceToChanges(change: OsmChangeAction, changeDescriptions: ChangeDescription[]) { | ||||
| 
 | ||||
|     private calculateDistanceToChanges( | ||||
|         change: OsmChangeAction, | ||||
|         changeDescriptions: ChangeDescription[] | ||||
|     ) { | ||||
|         const locations = this.historicalUserLocations?.features?.data | ||||
|         if (locations === undefined) { | ||||
|             // No state loaded or no locations -> we can't calculate...
 | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         if (!change.trackStatistics) { | ||||
|             // Probably irrelevant, such as a new helper node
 | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         const now = new Date() | ||||
|         const recentLocationPoints = locations.map(ff => ff.feature) | ||||
|             .filter(feat => feat.geometry.type === "Point") | ||||
|             .filter(feat => { | ||||
|                 const visitTime = new Date((<GeoLocationPointProperties><any>feat.properties).date) | ||||
|         const recentLocationPoints = locations | ||||
|             .map((ff) => ff.feature) | ||||
|             .filter((feat) => feat.geometry.type === "Point") | ||||
|             .filter((feat) => { | ||||
|                 const visitTime = new Date( | ||||
|                     (<GeoLocationPointProperties>(<any>feat.properties)).date | ||||
|                 ) | ||||
|                 // In seconds
 | ||||
|                 const diff = (now.getTime() - visitTime.getTime()) / 1000 | ||||
|                 return diff < Constants.nearbyVisitTime; | ||||
|                 return diff < Constants.nearbyVisitTime | ||||
|             }) | ||||
|         if (recentLocationPoints.length === 0) { | ||||
|             // Probably no GPS enabled/no fix
 | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         // The applicable points, contain information in their properties about location, time and GPS accuracy
 | ||||
|  | @ -182,7 +193,10 @@ export class Changes { | |||
|         } | ||||
| 
 | ||||
|         for (const changeDescription of changeDescriptions) { | ||||
|             const chng: { lat: number, lon: number } | { coordinates: [number, number][] } | { members } = changeDescription.changes | ||||
|             const chng: | ||||
|                 | { lat: number; lon: number } | ||||
|                 | { coordinates: [number, number][] } | ||||
|                 | { members } = changeDescription.changes | ||||
|             if (chng === undefined) { | ||||
|                 continue | ||||
|             } | ||||
|  | @ -194,61 +208,85 @@ export class Changes { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return Math.min(...changedObjectCoordinates.map(coor => | ||||
|             Math.min(...recentLocationPoints.map(gpsPoint => { | ||||
|         return Math.min( | ||||
|             ...changedObjectCoordinates.map((coor) => | ||||
|                 Math.min( | ||||
|                     ...recentLocationPoints.map((gpsPoint) => { | ||||
|                         const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint) | ||||
|                         return GeoOperations.distanceBetween(coor, otherCoor) | ||||
|             })) | ||||
|         )) | ||||
|                     }) | ||||
|                 ) | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * UPload the selected changes to OSM. | ||||
|      * Returns 'true' if successfull and if they can be removed | ||||
|      */ | ||||
|     private async flushSelectChanges(pending: ChangeDescription[], openChangeset: UIEventSource<number>): Promise<boolean> { | ||||
|         const self = this; | ||||
|     private async flushSelectChanges( | ||||
|         pending: ChangeDescription[], | ||||
|         openChangeset: UIEventSource<number> | ||||
|     ): Promise<boolean> { | ||||
|         const self = this | ||||
|         const neededIds = Changes.GetNeededIds(pending) | ||||
| 
 | ||||
|         const osmObjects = Utils.NoNull(await Promise.all(neededIds.map(async id => | ||||
|             OsmObject.DownloadObjectAsync(id).catch(e => { | ||||
|                 console.error("Could not download OSM-object", id, " dropping it from the changes ("+e+")") | ||||
|                 pending = pending.filter(ch => ch.type + "/" + ch.id !== id) | ||||
|                 return undefined; | ||||
|             })))); | ||||
|         const osmObjects = Utils.NoNull( | ||||
|             await Promise.all( | ||||
|                 neededIds.map(async (id) => | ||||
|                     OsmObject.DownloadObjectAsync(id).catch((e) => { | ||||
|                         console.error( | ||||
|                             "Could not download OSM-object", | ||||
|                             id, | ||||
|                             " dropping it from the changes (" + e + ")" | ||||
|                         ) | ||||
|                         pending = pending.filter((ch) => ch.type + "/" + ch.id !== id) | ||||
|                         return undefined | ||||
|                     }) | ||||
|                 ) | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         if (this._leftRightSensitive) { | ||||
|             osmObjects.forEach(obj => SimpleMetaTagger.removeBothTagging(obj.tags)) | ||||
|             osmObjects.forEach((obj) => SimpleMetaTagger.removeBothTagging(obj.tags)) | ||||
|         } | ||||
| 
 | ||||
|         console.log("Got the fresh objects!", osmObjects, "pending: ", pending) | ||||
|         if(pending.length == 0){ | ||||
|         if (pending.length == 0) { | ||||
|             console.log("No pending changes...") | ||||
|             return true; | ||||
|             return true | ||||
|         } | ||||
| 
 | ||||
|         const perType = Array.from( | ||||
|             Utils.Hist(pending.filter(descr => descr.meta.changeType !== undefined && descr.meta.changeType !== null) | ||||
|                 .map(descr => descr.meta.changeType)), ([key, count]) => ( | ||||
|                 { | ||||
|             Utils.Hist( | ||||
|                 pending | ||||
|                     .filter( | ||||
|                         (descr) => | ||||
|                             descr.meta.changeType !== undefined && descr.meta.changeType !== null | ||||
|                     ) | ||||
|                     .map((descr) => descr.meta.changeType) | ||||
|             ), | ||||
|             ([key, count]) => ({ | ||||
|                 key: key, | ||||
|                 value: count, | ||||
|                     aggregate: true | ||||
|                 })) | ||||
|         const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined) | ||||
|             .map(descr => ({ | ||||
|                 aggregate: true, | ||||
|             }) | ||||
|         ) | ||||
|         const motivations = pending | ||||
|             .filter((descr) => descr.meta.specialMotivation !== undefined) | ||||
|             .map((descr) => ({ | ||||
|                 key: descr.meta.changeType + ":" + descr.type + "/" + descr.id, | ||||
|                 value: descr.meta.specialMotivation | ||||
|                 value: descr.meta.specialMotivation, | ||||
|             })) | ||||
| 
 | ||||
|         const distances = Utils.NoNull(pending.map(descr => descr.meta.distanceToObject)); | ||||
|         const distances = Utils.NoNull(pending.map((descr) => descr.meta.distanceToObject)) | ||||
|         distances.sort((a, b) => a - b) | ||||
|         const perBinCount = Constants.distanceToChangeObjectBins.map(_ => 0) | ||||
|         const perBinCount = Constants.distanceToChangeObjectBins.map((_) => 0) | ||||
| 
 | ||||
|         let j = 0; | ||||
|         let j = 0 | ||||
|         const maxDistances = Constants.distanceToChangeObjectBins | ||||
|         for (let i = 0; i < maxDistances.length; i++) { | ||||
|             const maxDistance = maxDistances[i]; | ||||
|             const maxDistance = maxDistances[i] | ||||
|             // distances is sorted in ascending order, so as soon as one is to big, all the resting elements will be bigger too
 | ||||
|             while (j < distances.length && distances[j] < maxDistance) { | ||||
|                 perBinCount[i]++ | ||||
|  | @ -256,7 +294,8 @@ export class Changes { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const perBinMessage = Utils.NoNull(perBinCount.map((count, i) => { | ||||
|         const perBinMessage = Utils.NoNull( | ||||
|             perBinCount.map((count, i) => { | ||||
|                 if (count === 0) { | ||||
|                     return undefined | ||||
|                 } | ||||
|  | @ -268,9 +307,10 @@ export class Changes { | |||
|                 return { | ||||
|                     key, | ||||
|                     value: count, | ||||
|                 aggregate: true | ||||
|                     aggregate: true, | ||||
|                 } | ||||
|         })) | ||||
|             }) | ||||
|         ) | ||||
| 
 | ||||
|         // This method is only called with changedescriptions for this theme
 | ||||
|         const theme = pending[0].meta.theme | ||||
|  | @ -279,28 +319,29 @@ export class Changes { | |||
|             comment += "\n\n" + this.extraComment.data | ||||
|         } | ||||
| 
 | ||||
|         const metatags: ChangesetTag[] = [{ | ||||
|         const metatags: ChangesetTag[] = [ | ||||
|             { | ||||
|                 key: "comment", | ||||
|             value: comment | ||||
|                 value: comment, | ||||
|             }, | ||||
|             { | ||||
|                 key: "theme", | ||||
|                 value: theme | ||||
|                 value: theme, | ||||
|             }, | ||||
|             ...perType, | ||||
|             ...motivations, | ||||
|             ...perBinMessage | ||||
|             ...perBinMessage, | ||||
|         ] | ||||
| 
 | ||||
|         await this._changesetHandler.UploadChangeset( | ||||
|             (csId, remappings) =>{ | ||||
|                 if(remappings.size > 0){ | ||||
|             (csId, remappings) => { | ||||
|                 if (remappings.size > 0) { | ||||
|                     console.log("Rewriting pending changes from", pending, "with", remappings) | ||||
|                     pending = pending.map(ch => ChangeDescriptionTools.rewriteIds(ch, remappings)) | ||||
|                     pending = pending.map((ch) => ChangeDescriptionTools.rewriteIds(ch, remappings)) | ||||
|                     console.log("Result is", pending) | ||||
|                 } | ||||
|                 const changes: { | ||||
|                     newObjects: OsmObject[], | ||||
|                     newObjects: OsmObject[] | ||||
|                     modifiedObjects: OsmObject[] | ||||
|                     deletedObjects: OsmObject[] | ||||
|                 } = self.CreateChangesetObjects(pending, osmObjects) | ||||
|  | @ -311,14 +352,14 @@ export class Changes { | |||
|         ) | ||||
| 
 | ||||
|         console.log("Upload successfull!") | ||||
|         return true; | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     private async flushChangesAsync(): Promise<void> { | ||||
|         const self = this; | ||||
|         const self = this | ||||
|         try { | ||||
|             // At last, we build the changeset and upload
 | ||||
|             const pending = self.pendingChanges.data; | ||||
|             const pending = self.pendingChanges.data | ||||
| 
 | ||||
|             const pendingPerTheme = new Map<string, ChangeDescription[]>() | ||||
|             for (const changeDescription of pending) { | ||||
|  | @ -329,50 +370,62 @@ export class Changes { | |||
|                 pendingPerTheme.get(theme).push(changeDescription) | ||||
|             } | ||||
| 
 | ||||
|             const successes = await Promise.all(Array.from(pendingPerTheme, | ||||
|                 async ([theme, pendingChanges]) => { | ||||
|             const successes = await Promise.all( | ||||
|                 Array.from(pendingPerTheme, async ([theme, pendingChanges]) => { | ||||
|                     try { | ||||
|                         const openChangeset = this.state.osmConnection.GetPreference("current-open-changeset-" + theme).sync( | ||||
|                             str => { | ||||
|                                 const n = Number(str); | ||||
|                         const openChangeset = this.state.osmConnection | ||||
|                             .GetPreference("current-open-changeset-" + theme) | ||||
|                             .sync( | ||||
|                                 (str) => { | ||||
|                                     const n = Number(str) | ||||
|                                     if (isNaN(n)) { | ||||
|                                         return undefined | ||||
|                                     } | ||||
|                                     return n | ||||
|                             }, [], n => "" + n | ||||
|                         ); | ||||
|                         console.log("Using current-open-changeset-" + theme + " from the preferences, got " + openChangeset.data) | ||||
|                                 }, | ||||
|                                 [], | ||||
|                                 (n) => "" + n | ||||
|                             ) | ||||
|                         console.log( | ||||
|                             "Using current-open-changeset-" + | ||||
|                                 theme + | ||||
|                                 " from the preferences, got " + | ||||
|                                 openChangeset.data | ||||
|                         ) | ||||
| 
 | ||||
|                         return await self.flushSelectChanges(pendingChanges, openChangeset); | ||||
|                         return await self.flushSelectChanges(pendingChanges, openChangeset) | ||||
|                     } catch (e) { | ||||
|                         console.error("Could not upload some changes:", e) | ||||
|                         return false | ||||
|                     } | ||||
|                 })) | ||||
|                 }) | ||||
|             ) | ||||
| 
 | ||||
|             if (!successes.some(s => s == false)) { | ||||
|             if (!successes.some((s) => s == false)) { | ||||
|                 // All changes successfull, we clear the data!
 | ||||
|                 this.pendingChanges.setData([]); | ||||
|                 this.pendingChanges.setData([]) | ||||
|             } | ||||
| 
 | ||||
|         } catch (e) { | ||||
|             console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e) | ||||
|             console.error( | ||||
|                 "Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", | ||||
|                 e | ||||
|             ) | ||||
|             self.pendingChanges.setData([]) | ||||
|         } finally { | ||||
|             self.isUploading.setData(false) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): { | ||||
|         newObjects: OsmObject[], | ||||
|     public CreateChangesetObjects( | ||||
|         changes: ChangeDescription[], | ||||
|         downloadedOsmObjects: OsmObject[] | ||||
|     ): { | ||||
|         newObjects: OsmObject[] | ||||
|         modifiedObjects: OsmObject[] | ||||
|         deletedObjects: OsmObject[] | ||||
| 
 | ||||
|     } { | ||||
|         const objects: Map<string, OsmObject> = new Map<string, OsmObject>() | ||||
|         const states: Map<string, "unchanged" | "created" | "modified" | "deleted"> = new Map(); | ||||
|         const states: Map<string, "unchanged" | "created" | "modified" | "deleted"> = new Map() | ||||
| 
 | ||||
|         for (const o of downloadedOsmObjects) { | ||||
|             objects.set(o.type + "/" + o.id, o) | ||||
|  | @ -385,7 +438,7 @@ export class Changes { | |||
|         } | ||||
| 
 | ||||
|         for (const change of changes) { | ||||
|             let changed = false; | ||||
|             let changed = false | ||||
|             const id = change.type + "/" + change.id | ||||
|             if (!objects.has(id)) { | ||||
|                 // The object hasn't been seen before, so it doesn't exist yet and is newly created by its very definition
 | ||||
|  | @ -400,24 +453,24 @@ export class Changes { | |||
|                 // This is a new object that should be created
 | ||||
|                 states.set(id, "created") | ||||
|                 console.log("Creating object for changeDescription", change) | ||||
|                 let osmObj: OsmObject = undefined; | ||||
|                 let osmObj: OsmObject = undefined | ||||
|                 switch (change.type) { | ||||
|                     case "node": | ||||
|                         const n = new OsmNode(change.id) | ||||
|                         n.lat = change.changes["lat"] | ||||
|                         n.lon = change.changes["lon"] | ||||
|                         osmObj = n | ||||
|                         break; | ||||
|                         break | ||||
|                     case "way": | ||||
|                         const w = new OsmWay(change.id) | ||||
|                         w.nodes = change.changes["nodes"] | ||||
|                         osmObj = w | ||||
|                         break; | ||||
|                         break | ||||
|                     case "relation": | ||||
|                         const r = new OsmRelation(change.id) | ||||
|                         r.members = change.changes["members"] | ||||
|                         osmObj = r | ||||
|                         break; | ||||
|                         break | ||||
|                 } | ||||
|                 if (osmObj === undefined) { | ||||
|                     throw "Hmm? This is a bug" | ||||
|  | @ -442,55 +495,57 @@ export class Changes { | |||
|                 let v = kv.v | ||||
| 
 | ||||
|                 if (v === "") { | ||||
|                     v = undefined; | ||||
|                     v = undefined | ||||
|                 } | ||||
| 
 | ||||
|                 const oldV = obj.tags[k] | ||||
|                 if (oldV === v) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 obj.tags[k] = v; | ||||
|                 changed = true; | ||||
| 
 | ||||
| 
 | ||||
|                 obj.tags[k] = v | ||||
|                 changed = true | ||||
|             } | ||||
| 
 | ||||
|             if (change.changes !== undefined) { | ||||
|                 switch (change.type) { | ||||
|                     case "node": | ||||
|                         // @ts-ignore
 | ||||
|                         const nlat = change.changes.lat; | ||||
|                         const nlat = change.changes.lat | ||||
|                         // @ts-ignore
 | ||||
|                         const nlon = change.changes.lon; | ||||
|                         const nlon = change.changes.lon | ||||
|                         const n = <OsmNode>obj | ||||
|                         if (n.lat !== nlat || n.lon !== nlon) { | ||||
|                             n.lat = nlat; | ||||
|                             n.lon = nlon; | ||||
|                             changed = true; | ||||
|                             n.lat = nlat | ||||
|                             n.lon = nlon | ||||
|                             changed = true | ||||
|                         } | ||||
|                         break; | ||||
|                         break | ||||
|                     case "way": | ||||
|                         const nnodes = change.changes["nodes"] | ||||
|                         const w = <OsmWay>obj | ||||
|                         if (!Utils.Identical(nnodes, w.nodes)) { | ||||
|                             w.nodes = nnodes | ||||
|                             changed = true; | ||||
|                             changed = true | ||||
|                         } | ||||
|                         break; | ||||
|                         break | ||||
|                     case "relation": | ||||
|                         const nmembers: { type: "node" | "way" | "relation", ref: number, role: string }[] = change.changes["members"] | ||||
|                         const nmembers: { | ||||
|                             type: "node" | "way" | "relation" | ||||
|                             ref: number | ||||
|                             role: string | ||||
|                         }[] = change.changes["members"] | ||||
|                         const r = <OsmRelation>obj | ||||
|                         if (!Utils.Identical(nmembers, r.members, (a, b) => { | ||||
|                         if ( | ||||
|                             !Utils.Identical(nmembers, r.members, (a, b) => { | ||||
|                                 return a.role === b.role && a.type === b.type && a.ref === b.ref | ||||
|                         })) { | ||||
|                             r.members = nmembers; | ||||
|                             changed = true; | ||||
|                             }) | ||||
|                         ) { | ||||
|                             r.members = nmembers | ||||
|                             changed = true | ||||
|                         } | ||||
|                         break; | ||||
| 
 | ||||
|                         break | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             if (changed && states.get(id) === "unchanged") { | ||||
|  | @ -498,15 +553,13 @@ export class Changes { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const result = { | ||||
|             newObjects: [], | ||||
|             modifiedObjects: [], | ||||
|             deletedObjects: [] | ||||
|             deletedObjects: [], | ||||
|         } | ||||
| 
 | ||||
|         objects.forEach((v, id) => { | ||||
| 
 | ||||
|             const state = states.get(id) | ||||
|             if (state === "created") { | ||||
|                 result.newObjects.push(v) | ||||
|  | @ -517,14 +570,21 @@ export class Changes { | |||
|             if (state === "deleted") { | ||||
|                 result.deletedObjects.push(v) | ||||
|             } | ||||
| 
 | ||||
|         }) | ||||
| 
 | ||||
|         console.debug("Calculated the pending changes: ", result.newObjects.length, "new; ", result.modifiedObjects.length, "modified;", result.deletedObjects, "deleted") | ||||
|         console.debug( | ||||
|             "Calculated the pending changes: ", | ||||
|             result.newObjects.length, | ||||
|             "new; ", | ||||
|             result.modifiedObjects.length, | ||||
|             "modified;", | ||||
|             result.deletedObjects, | ||||
|             "deleted" | ||||
|         ) | ||||
|         return result | ||||
|     } | ||||
| 
 | ||||
|     public setHistoricalUserLocations(locations: FeatureSource ){ | ||||
|     public setHistoricalUserLocations(locations: FeatureSource) { | ||||
|         this.historicalUserLocations = locations | ||||
|     } | ||||
| } | ||||
|  | @ -1,28 +1,26 @@ | |||
| import escapeHtml from "escape-html"; | ||||
| import UserDetails, {OsmConnection} from "./OsmConnection"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {ElementStorage} from "../ElementStorage"; | ||||
| import Locale from "../../UI/i18n/Locale"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import {Changes} from "./Changes"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import escapeHtml from "escape-html" | ||||
| import UserDetails, { OsmConnection } from "./OsmConnection" | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import { ElementStorage } from "../ElementStorage" | ||||
| import Locale from "../../UI/i18n/Locale" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { Changes } from "./Changes" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| export interface ChangesetTag { | ||||
|     key: string, | ||||
|     value: string | number, | ||||
|     key: string | ||||
|     value: string | number | ||||
|     aggregate?: boolean | ||||
| } | ||||
| 
 | ||||
| export class ChangesetHandler { | ||||
| 
 | ||||
|     private readonly allElements: ElementStorage; | ||||
|     private osmConnection: OsmConnection; | ||||
|     private readonly changes: Changes; | ||||
|     private readonly _dryRun: UIEventSource<boolean>; | ||||
|     private readonly userDetails: UIEventSource<UserDetails>; | ||||
|     private readonly auth: any; | ||||
|     private readonly backend: string; | ||||
| 
 | ||||
|     private readonly allElements: ElementStorage | ||||
|     private osmConnection: OsmConnection | ||||
|     private readonly changes: Changes | ||||
|     private readonly _dryRun: UIEventSource<boolean> | ||||
|     private readonly userDetails: UIEventSource<UserDetails> | ||||
|     private readonly auth: any | ||||
|     private readonly backend: string | ||||
| 
 | ||||
|     /** | ||||
|      * Contains previously rewritten IDs | ||||
|  | @ -30,7 +28,6 @@ export class ChangesetHandler { | |||
|      */ | ||||
|     private readonly _remappings = new Map<string, string>() | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Use 'osmConnection.CreateChangesetHandler' instead | ||||
|      * @param dryRun | ||||
|  | @ -39,36 +36,36 @@ export class ChangesetHandler { | |||
|      * @param changes | ||||
|      * @param auth | ||||
|      */ | ||||
|     constructor(dryRun: UIEventSource<boolean>, | ||||
|     constructor( | ||||
|         dryRun: UIEventSource<boolean>, | ||||
|         osmConnection: OsmConnection, | ||||
|         allElements: ElementStorage, | ||||
|         changes: Changes, | ||||
|                 auth) { | ||||
|         this.osmConnection = osmConnection; | ||||
|         this.allElements = allElements; | ||||
|         this.changes = changes; | ||||
|         this._dryRun = dryRun; | ||||
|         this.userDetails = osmConnection.userDetails; | ||||
|         auth | ||||
|     ) { | ||||
|         this.osmConnection = osmConnection | ||||
|         this.allElements = allElements | ||||
|         this.changes = changes | ||||
|         this._dryRun = dryRun | ||||
|         this.userDetails = osmConnection.userDetails | ||||
|         this.backend = osmConnection._oauth_config.url | ||||
|         this.auth = auth; | ||||
|         this.auth = auth | ||||
| 
 | ||||
|         if (dryRun) { | ||||
|             console.log("DRYRUN ENABLED"); | ||||
|             console.log("DRYRUN ENABLED") | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|      | ||||
|     /** | ||||
|      * Creates a new list which contains every key at most once | ||||
|      * | ||||
|      * ChangesetHandler.removeDuplicateMetaTags([{key: "k", value: "v"}, {key: "k0", value: "v0"}, {key: "k", value:"v"}] // => [{key: "k", value: "v"}, {key: "k0", value: "v0"}]
 | ||||
|      */ | ||||
|     public static removeDuplicateMetaTags(extraMetaTags: ChangesetTag[]): ChangesetTag[]{ | ||||
|         const r : ChangesetTag[] = [] | ||||
|     public static removeDuplicateMetaTags(extraMetaTags: ChangesetTag[]): ChangesetTag[] { | ||||
|         const r: ChangesetTag[] = [] | ||||
|         const seen = new Set<string>() | ||||
|         for (const extraMetaTag of extraMetaTags) { | ||||
|             if(seen.has(extraMetaTag.key)){ | ||||
|             if (seen.has(extraMetaTag.key)) { | ||||
|                 continue | ||||
|             } | ||||
|             r.push(extraMetaTag) | ||||
|  | @ -86,7 +83,7 @@ export class ChangesetHandler { | |||
|      * @private | ||||
|      */ | ||||
|     static rewriteMetaTags(extraMetaTags: ChangesetTag[], rewriteIds: Map<string, string>) { | ||||
|         let hasChange = false; | ||||
|         let hasChange = false | ||||
|         for (const tag of extraMetaTags) { | ||||
|             const match = tag.key.match(/^([a-zA-Z0-9_]+):(node\/-[0-9])$/) | ||||
|             if (match == null) { | ||||
|  | @ -115,9 +112,12 @@ export class ChangesetHandler { | |||
|     public async UploadChangeset( | ||||
|         generateChangeXML: (csid: number, remappings: Map<string, string>) => string, | ||||
|         extraMetaTags: ChangesetTag[], | ||||
|         openChangeset: UIEventSource<number>): Promise<void> { | ||||
| 
 | ||||
|         if (!extraMetaTags.some(tag => tag.key === "comment") || !extraMetaTags.some(tag => tag.key === "theme")) { | ||||
|         openChangeset: UIEventSource<number> | ||||
|     ): Promise<void> { | ||||
|         if ( | ||||
|             !extraMetaTags.some((tag) => tag.key === "comment") || | ||||
|             !extraMetaTags.some((tag) => tag.key === "theme") | ||||
|         ) { | ||||
|             throw "The meta tags should at least contain a `comment` and a `theme`" | ||||
|         } | ||||
| 
 | ||||
|  | @ -125,30 +125,35 @@ export class ChangesetHandler { | |||
|         extraMetaTags = ChangesetHandler.removeDuplicateMetaTags(extraMetaTags) | ||||
|         if (this.userDetails.data.csCount == 0) { | ||||
|             // The user became a contributor!
 | ||||
|             this.userDetails.data.csCount = 1; | ||||
|             this.userDetails.ping(); | ||||
|             this.userDetails.data.csCount = 1 | ||||
|             this.userDetails.ping() | ||||
|         } | ||||
|         if (this._dryRun.data) { | ||||
|             const changesetXML = generateChangeXML(123456, this._remappings); | ||||
|             const changesetXML = generateChangeXML(123456, this._remappings) | ||||
|             console.log("Metatags are", extraMetaTags) | ||||
|             console.log(changesetXML); | ||||
|             return; | ||||
|             console.log(changesetXML) | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (openChangeset.data === undefined) { | ||||
|             // We have to open a new changeset
 | ||||
|             try { | ||||
|                 const csId = await this.OpenChangeset(extraMetaTags) | ||||
|                 openChangeset.setData(csId); | ||||
|                 const changeset = generateChangeXML(csId, this._remappings); | ||||
|                 console.trace("Opened a new changeset (openChangeset.data is undefined):", changeset); | ||||
|                 openChangeset.setData(csId) | ||||
|                 const changeset = generateChangeXML(csId, this._remappings) | ||||
|                 console.trace( | ||||
|                     "Opened a new changeset (openChangeset.data is undefined):", | ||||
|                     changeset | ||||
|                 ) | ||||
|                 const changes = await this.UploadChange(csId, changeset) | ||||
|                 const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(extraMetaTags, changes) | ||||
|                 if(hasSpecialMotivationChanges){ | ||||
|                 const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags( | ||||
|                     extraMetaTags, | ||||
|                     changes | ||||
|                 ) | ||||
|                 if (hasSpecialMotivationChanges) { | ||||
|                     // At this point, 'extraMetaTags' will have changed - we need to set the tags again
 | ||||
|                     this.UpdateTags(csId, extraMetaTags) | ||||
|                 } | ||||
|                  | ||||
|             } catch (e) { | ||||
|                 console.error("Could not open/upload changeset due to ", e) | ||||
|                 openChangeset.setData(undefined) | ||||
|  | @ -156,29 +161,32 @@ export class ChangesetHandler { | |||
|         } else { | ||||
|             // There still exists an open changeset (or at least we hope so)
 | ||||
|             // Let's check!
 | ||||
|             const csId = openChangeset.data; | ||||
|             const csId = openChangeset.data | ||||
|             try { | ||||
| 
 | ||||
|                 const oldChangesetMeta = await this.GetChangesetMeta(csId) | ||||
|                 if (!oldChangesetMeta.open) { | ||||
|                     // Mark the CS as closed...
 | ||||
|                     console.log("Could not fetch the metadata from the already open changeset") | ||||
|                     openChangeset.setData(undefined); | ||||
|                     openChangeset.setData(undefined) | ||||
|                     // ... and try again. As the cs is closed, no recursive loop can exist
 | ||||
|                     await this.UploadChangeset(generateChangeXML, extraMetaTags, openChangeset) | ||||
|                     return; | ||||
|                     return | ||||
|                 } | ||||
| 
 | ||||
|                 const rewritings = await this.UploadChange( | ||||
|                     csId, | ||||
|                     generateChangeXML(csId, this._remappings)) | ||||
|                     generateChangeXML(csId, this._remappings) | ||||
|                 ) | ||||
| 
 | ||||
|                 const rewrittenTags = this.RewriteTagsOf(extraMetaTags, rewritings, oldChangesetMeta) | ||||
|                 const rewrittenTags = this.RewriteTagsOf( | ||||
|                     extraMetaTags, | ||||
|                     rewritings, | ||||
|                     oldChangesetMeta | ||||
|                 ) | ||||
|                 await this.UpdateTags(csId, rewrittenTags) | ||||
| 
 | ||||
|             } catch (e) { | ||||
|                 console.warn("Could not upload, changeset is probably closed: ", e); | ||||
|                 openChangeset.setData(undefined); | ||||
|                 console.warn("Could not upload, changeset is probably closed: ", e) | ||||
|                 openChangeset.setData(undefined) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | @ -190,17 +198,17 @@ export class ChangesetHandler { | |||
|      * @param rewriteIds: the mapping of ids | ||||
|      * @param oldChangesetMeta: the metadata-object of the already existing changeset | ||||
|      */ | ||||
|     public RewriteTagsOf(extraMetaTags: ChangesetTag[], | ||||
|     public RewriteTagsOf( | ||||
|         extraMetaTags: ChangesetTag[], | ||||
|         rewriteIds: Map<string, string>, | ||||
|         oldChangesetMeta: { | ||||
|                                     open: boolean, | ||||
|             open: boolean | ||||
|             id: number | ||||
|                                     uid: number, // User ID
 | ||||
|                                     changes_count: number, | ||||
|             uid: number // User ID
 | ||||
|             changes_count: number | ||||
|             tags: any | ||||
|                                 }) : ChangesetTag[] { | ||||
| 
 | ||||
| 
 | ||||
|         } | ||||
|     ): ChangesetTag[] { | ||||
|         // Note: extraMetaTags is where all the tags are collected into
 | ||||
| 
 | ||||
|         // same as 'extraMetaTag', but indexed
 | ||||
|  | @ -221,7 +229,7 @@ export class ChangesetHandler { | |||
|             if (newMetaTag === undefined) { | ||||
|                 extraMetaTags.push({ | ||||
|                     key: key, | ||||
|                     value: oldCsTags[key] | ||||
|                     value: oldCsTags[key], | ||||
|                 }) | ||||
|                 continue | ||||
|             } | ||||
|  | @ -242,10 +250,8 @@ export class ChangesetHandler { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         ChangesetHandler.rewriteMetaTags(extraMetaTags, rewriteIds) | ||||
|         return extraMetaTags | ||||
|      | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -255,18 +261,18 @@ export class ChangesetHandler { | |||
|      * @private | ||||
|      */ | ||||
|     private static parseIdRewrite(node: any, type: string): [string, string] { | ||||
|         const oldId = parseInt(node.attributes.old_id.value); | ||||
|         const oldId = parseInt(node.attributes.old_id.value) | ||||
|         if (node.attributes.new_id === undefined) { | ||||
|             return [type+"/"+oldId, undefined]; | ||||
|             return [type + "/" + oldId, undefined] | ||||
|         } | ||||
| 
 | ||||
|         const newId = parseInt(node.attributes.new_id.value); | ||||
|         const newId = parseInt(node.attributes.new_id.value) | ||||
|         // The actual mapping
 | ||||
|         const result: [string, string] = [type + "/" + oldId, type + "/" + newId] | ||||
|         if(oldId === newId){ | ||||
|             return undefined; | ||||
|         if (oldId === newId) { | ||||
|             return undefined | ||||
|         } | ||||
|         return result; | ||||
|         return result | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -284,8 +290,8 @@ export class ChangesetHandler { | |||
|      * @private | ||||
|      */ | ||||
|     private parseUploadChangesetResponse(response: XMLDocument): Map<string, string> { | ||||
|         const nodes = response.getElementsByTagName("node"); | ||||
|         const mappings : [string, string][]= [] | ||||
|         const nodes = response.getElementsByTagName("node") | ||||
|         const mappings: [string, string][] = [] | ||||
| 
 | ||||
|         for (const node of Array.from(nodes)) { | ||||
|             const mapping = ChangesetHandler.parseIdRewrite(node, "node") | ||||
|  | @ -294,7 +300,7 @@ export class ChangesetHandler { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const ways = response.getElementsByTagName("way"); | ||||
|         const ways = response.getElementsByTagName("way") | ||||
|         for (const way of Array.from(ways)) { | ||||
|             const mapping = ChangesetHandler.parseIdRewrite(way, "way") | ||||
|             if (mapping !== undefined) { | ||||
|  | @ -303,40 +309,41 @@ export class ChangesetHandler { | |||
|         } | ||||
|         for (const mapping of mappings) { | ||||
|             const [oldId, newId] = mapping | ||||
|             this.allElements.addAlias(oldId, newId); | ||||
|             if(newId !== undefined) { | ||||
|             this.allElements.addAlias(oldId, newId) | ||||
|             if (newId !== undefined) { | ||||
|                 this._remappings.set(mapping[0], mapping[1]) | ||||
|             } | ||||
|         } | ||||
|         return new Map<string, string>(mappings) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private async CloseChangeset(changesetId: number = undefined): Promise<void> { | ||||
|         const self = this | ||||
|         return new Promise<void>(function (resolve, reject) { | ||||
|             if (changesetId === undefined) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             self.auth.xhr({ | ||||
|                 method: 'PUT', | ||||
|                 path: '/api/0.6/changeset/' + changesetId + '/close', | ||||
|             }, function (err, response) { | ||||
|             self.auth.xhr( | ||||
|                 { | ||||
|                     method: "PUT", | ||||
|                     path: "/api/0.6/changeset/" + changesetId + "/close", | ||||
|                 }, | ||||
|                 function (err, response) { | ||||
|                     if (response == null) { | ||||
| 
 | ||||
|                     console.log("err", err); | ||||
|                         console.log("err", err) | ||||
|                     } | ||||
|                     console.log("Closed changeset ", changesetId) | ||||
|                     resolve() | ||||
|             }); | ||||
|                 } | ||||
|             ) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     async GetChangesetMeta(csId: number): Promise<{ | ||||
|         id: number, | ||||
|         open: boolean, | ||||
|         uid: number, | ||||
|         changes_count: number, | ||||
|         id: number | ||||
|         open: boolean | ||||
|         uid: number | ||||
|         changes_count: number | ||||
|         tags: any | ||||
|     }> { | ||||
|         const url = `${this.backend}/api/0.6/changeset/${csId}` | ||||
|  | @ -344,47 +351,59 @@ export class ChangesetHandler { | |||
|         return csData.elements[0] | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Puts the specified tags onto the changesets as they are. | ||||
|      * This method will erase previously set tags | ||||
|      */ | ||||
|     private async UpdateTags( | ||||
|         csId: number, | ||||
|         tags: ChangesetTag[]) { | ||||
|     private async UpdateTags(csId: number, tags: ChangesetTag[]) { | ||||
|         tags = ChangesetHandler.removeDuplicateMetaTags(tags) | ||||
| 
 | ||||
|         const self = this; | ||||
|         const self = this | ||||
|         return new Promise<string>(function (resolve, reject) { | ||||
|             tags = Utils.NoNull(tags).filter( | ||||
|                 (tag) => | ||||
|                     tag.key !== undefined && | ||||
|                     tag.value !== undefined && | ||||
|                     tag.key !== "" && | ||||
|                     tag.value !== "" | ||||
|             ) | ||||
|             const metadata = tags.map((kv) => `<tag k="${kv.key}" v="${escapeHtml(kv.value)}"/>`) | ||||
| 
 | ||||
|             tags = Utils.NoNull(tags).filter(tag => tag.key !== undefined && tag.value !== undefined && tag.key !== "" && tag.value !== "") | ||||
|             const metadata = tags.map(kv => `<tag k="${kv.key}" v="${escapeHtml(kv.value)}"/>`) | ||||
| 
 | ||||
|             self.auth.xhr({ | ||||
|                 method: 'PUT', | ||||
|                 path: '/api/0.6/changeset/' + csId, | ||||
|                 options: {header: {'Content-Type': 'text/xml'}}, | ||||
|                 content: [`<osm><changeset>`, | ||||
|                     metadata, | ||||
|                     `</changeset></osm>`].join("") | ||||
|             }, function (err, response) { | ||||
|             self.auth.xhr( | ||||
|                 { | ||||
|                     method: "PUT", | ||||
|                     path: "/api/0.6/changeset/" + csId, | ||||
|                     options: { header: { "Content-Type": "text/xml" } }, | ||||
|                     content: [`<osm><changeset>`, metadata, `</changeset></osm>`].join(""), | ||||
|                 }, | ||||
|                 function (err, response) { | ||||
|                     if (response === undefined) { | ||||
|                     console.error("Updating the tags of changeset "+csId+" failed:", err); | ||||
|                         console.error("Updating the tags of changeset " + csId + " failed:", err) | ||||
|                         reject(err) | ||||
|                     } else { | ||||
|                     resolve(response); | ||||
|                         resolve(response) | ||||
|                     } | ||||
|             }); | ||||
|                 } | ||||
|             ) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private defaultChangesetTags() : ChangesetTag[]{ | ||||
|       return  [ ["created_by", `MapComplete ${Constants.vNumber}`], | ||||
|     private defaultChangesetTags(): ChangesetTag[] { | ||||
|         return [ | ||||
|             ["created_by", `MapComplete ${Constants.vNumber}`], | ||||
|             ["locale", Locale.language.data], | ||||
|             ["host", `${window.location.origin}${window.location.pathname}`], | ||||
|             ["source", this.changes.state["currentUserLocation"]?.features?.data?.length > 0 ? "survey" : undefined], | ||||
|             ["imagery", this.changes.state["backgroundLayer"]?.data?.id]].map(([key, value]) => ({ | ||||
|             key, value, aggretage: false | ||||
|             [ | ||||
|                 "source", | ||||
|                 this.changes.state["currentUserLocation"]?.features?.data?.length > 0 | ||||
|                     ? "survey" | ||||
|                     : undefined, | ||||
|             ], | ||||
|             ["imagery", this.changes.state["backgroundLayer"]?.data?.id], | ||||
|         ].map(([key, value]) => ({ | ||||
|             key, | ||||
|             value, | ||||
|             aggretage: false, | ||||
|         })) | ||||
|     } | ||||
| 
 | ||||
|  | @ -394,61 +413,57 @@ export class ChangesetHandler { | |||
|      * @constructor | ||||
|      * @private | ||||
|      */ | ||||
|     private OpenChangeset( | ||||
|         changesetTags: ChangesetTag[] | ||||
|     ): Promise<number> { | ||||
|         const self = this; | ||||
|     private OpenChangeset(changesetTags: ChangesetTag[]): Promise<number> { | ||||
|         const self = this | ||||
|         return new Promise<number>(function (resolve, reject) { | ||||
| 
 | ||||
|             const metadata = changesetTags.map(cstag => [cstag.key, cstag.value]) | ||||
|                 .filter(kv => (kv[1] ?? "") !== "") | ||||
|                 .map(kv => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`) | ||||
|             const metadata = changesetTags | ||||
|                 .map((cstag) => [cstag.key, cstag.value]) | ||||
|                 .filter((kv) => (kv[1] ?? "") !== "") | ||||
|                 .map((kv) => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`) | ||||
|                 .join("\n") | ||||
| 
 | ||||
| 
 | ||||
|             self.auth.xhr({ | ||||
|                 method: 'PUT', | ||||
|                 path: '/api/0.6/changeset/create', | ||||
|                 options: {header: {'Content-Type': 'text/xml'}}, | ||||
|                 content: [`<osm><changeset>`, | ||||
|                     metadata, | ||||
|                     `</changeset></osm>`].join("") | ||||
|             }, function (err, response) { | ||||
|             self.auth.xhr( | ||||
|                 { | ||||
|                     method: "PUT", | ||||
|                     path: "/api/0.6/changeset/create", | ||||
|                     options: { header: { "Content-Type": "text/xml" } }, | ||||
|                     content: [`<osm><changeset>`, metadata, `</changeset></osm>`].join(""), | ||||
|                 }, | ||||
|                 function (err, response) { | ||||
|                     if (response === undefined) { | ||||
|                     console.error("Opening a changeset failed:", err); | ||||
|                         console.error("Opening a changeset failed:", err) | ||||
|                         reject(err) | ||||
|                     } else { | ||||
|                     resolve(Number(response)); | ||||
|                         resolve(Number(response)) | ||||
|                     } | ||||
|             }); | ||||
|                 } | ||||
|             ) | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Upload a changesetXML | ||||
|      */ | ||||
|     private UploadChange(changesetId: number, | ||||
|                       changesetXML: string): Promise<Map<string, string>> { | ||||
|         const self = this; | ||||
|     private UploadChange(changesetId: number, changesetXML: string): Promise<Map<string, string>> { | ||||
|         const self = this | ||||
|         return new Promise(function (resolve, reject) { | ||||
|             self.auth.xhr({ | ||||
|                 method: 'POST', | ||||
|                 options: {header: {'Content-Type': 'text/xml'}}, | ||||
|                 path: '/api/0.6/changeset/' + changesetId + '/upload', | ||||
|                 content: changesetXML | ||||
|             }, function (err, response) { | ||||
|             self.auth.xhr( | ||||
|                 { | ||||
|                     method: "POST", | ||||
|                     options: { header: { "Content-Type": "text/xml" } }, | ||||
|                     path: "/api/0.6/changeset/" + changesetId + "/upload", | ||||
|                     content: changesetXML, | ||||
|                 }, | ||||
|                 function (err, response) { | ||||
|                     if (response == null) { | ||||
|                     console.error("Uploading an actual change failed", err); | ||||
|                     reject(err); | ||||
|                         console.error("Uploading an actual change failed", err) | ||||
|                         reject(err) | ||||
|                     } | ||||
|                 const changes = self.parseUploadChangesetResponse(response); | ||||
|                 console.log("Uploaded changeset ", changesetId); | ||||
|                 resolve(changes); | ||||
|             }); | ||||
|                     const changes = self.parseUploadChangesetResponse(response) | ||||
|                     console.log("Uploaded changeset ", changesetId) | ||||
|                     resolve(changes) | ||||
|                 } | ||||
|             ) | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,23 +1,27 @@ | |||
| import State from "../../State"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {BBox} from "../BBox"; | ||||
| import State from "../../State" | ||||
| import { Utils } from "../../Utils" | ||||
| import { BBox } from "../BBox" | ||||
| 
 | ||||
| export interface GeoCodeResult { | ||||
|     display_name: string, | ||||
|     lat: number, lon: number, boundingbox: number[], | ||||
|     osm_type: "node" | "way" | "relation", | ||||
|     display_name: string | ||||
|     lat: number | ||||
|     lon: number | ||||
|     boundingbox: number[] | ||||
|     osm_type: "node" | "way" | "relation" | ||||
|     osm_id: string | ||||
| } | ||||
| 
 | ||||
| export class Geocoding { | ||||
| 
 | ||||
|     private static readonly host = "https://nominatim.openstreetmap.org/search?"; | ||||
|     private static readonly host = "https://nominatim.openstreetmap.org/search?" | ||||
| 
 | ||||
|     static async Search(query: string): Promise<GeoCodeResult[]> { | ||||
|         const b = State?.state?.currentBounds?.data ?? BBox.global; | ||||
|         const url = Geocoding.host + "format=json&limit=1&viewbox=" + | ||||
|         const b = State?.state?.currentBounds?.data ?? BBox.global | ||||
|         const url = | ||||
|             Geocoding.host + | ||||
|             "format=json&limit=1&viewbox=" + | ||||
|             `${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` + | ||||
|             "&accept-language=nl&q=" + query; | ||||
|             "&accept-language=nl&q=" + | ||||
|             query | ||||
|         return Utils.downloadJson(url) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,153 +1,161 @@ | |||
| import osmAuth from "osm-auth"; | ||||
| import {Store, Stores, UIEventSource} from "../UIEventSource"; | ||||
| import {OsmPreferences} from "./OsmPreferences"; | ||||
| import {ChangesetHandler} from "./ChangesetHandler"; | ||||
| import {ElementStorage} from "../ElementStorage"; | ||||
| import Svg from "../../Svg"; | ||||
| import Img from "../../UI/Base/Img"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {OsmObject} from "./OsmObject"; | ||||
| import {Changes} from "./Changes"; | ||||
| import osmAuth from "osm-auth" | ||||
| import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import { OsmPreferences } from "./OsmPreferences" | ||||
| import { ChangesetHandler } from "./ChangesetHandler" | ||||
| import { ElementStorage } from "../ElementStorage" | ||||
| import Svg from "../../Svg" | ||||
| import Img from "../../UI/Base/Img" | ||||
| import { Utils } from "../../Utils" | ||||
| import { OsmObject } from "./OsmObject" | ||||
| import { Changes } from "./Changes" | ||||
| 
 | ||||
| export default class UserDetails { | ||||
| 
 | ||||
|     public loggedIn = false; | ||||
|     public name = "Not logged in"; | ||||
|     public uid: number; | ||||
|     public csCount = 0; | ||||
|     public img: string; | ||||
|     public unreadMessages = 0; | ||||
|     public totalMessages = 0; | ||||
|     home: { lon: number; lat: number }; | ||||
|     public backend: string; | ||||
|     public loggedIn = false | ||||
|     public name = "Not logged in" | ||||
|     public uid: number | ||||
|     public csCount = 0 | ||||
|     public img: string | ||||
|     public unreadMessages = 0 | ||||
|     public totalMessages = 0 | ||||
|     home: { lon: number; lat: number } | ||||
|     public backend: string | ||||
| 
 | ||||
|     constructor(backend: string) { | ||||
|         this.backend = backend; | ||||
|         this.backend = backend | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class OsmConnection { | ||||
| 
 | ||||
|     public static readonly oauth_configs = { | ||||
|         "osm": { | ||||
|             oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem', | ||||
|             oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI', | ||||
|             url: "https://www.openstreetmap.org" | ||||
|         osm: { | ||||
|             oauth_consumer_key: "hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem", | ||||
|             oauth_secret: "wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI", | ||||
|             url: "https://www.openstreetmap.org", | ||||
|         }, | ||||
|         "osm-test": { | ||||
|             oauth_consumer_key: 'Zgr7EoKb93uwPv2EOFkIlf3n9NLwj5wbyfjZMhz2', | ||||
|             oauth_secret: '3am1i1sykHDMZ66SGq4wI2Z7cJMKgzneCHp3nctn', | ||||
|             url: "https://master.apis.dev.openstreetmap.org" | ||||
|             oauth_consumer_key: "Zgr7EoKb93uwPv2EOFkIlf3n9NLwj5wbyfjZMhz2", | ||||
|             oauth_secret: "3am1i1sykHDMZ66SGq4wI2Z7cJMKgzneCHp3nctn", | ||||
|             url: "https://master.apis.dev.openstreetmap.org", | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
|     public auth; | ||||
|     public userDetails: UIEventSource<UserDetails>; | ||||
|     public auth | ||||
|     public userDetails: UIEventSource<UserDetails> | ||||
|     public isLoggedIn: Store<boolean> | ||||
|     public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">("not-attempted") | ||||
|     public preferencesHandler: OsmPreferences; | ||||
|     public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">( | ||||
|         "not-attempted" | ||||
|     ) | ||||
|     public preferencesHandler: OsmPreferences | ||||
|     public readonly _oauth_config: { | ||||
|         oauth_consumer_key: string, | ||||
|         oauth_secret: string, | ||||
|         oauth_consumer_key: string | ||||
|         oauth_secret: string | ||||
|         url: string | ||||
|     }; | ||||
|     private readonly _dryRun: UIEventSource<boolean>; | ||||
|     private fakeUser: boolean; | ||||
|     private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []; | ||||
|     private readonly _iframeMode: Boolean | boolean; | ||||
|     private readonly _singlePage: boolean; | ||||
|     private isChecking = false; | ||||
|     } | ||||
|     private readonly _dryRun: UIEventSource<boolean> | ||||
|     private fakeUser: boolean | ||||
|     private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [] | ||||
|     private readonly _iframeMode: Boolean | boolean | ||||
|     private readonly _singlePage: boolean | ||||
|     private isChecking = false | ||||
| 
 | ||||
|     constructor(options: { | ||||
|                     dryRun?: UIEventSource<boolean>, | ||||
|                     fakeUser?: false | boolean, | ||||
|                     oauth_token?: UIEventSource<string>, | ||||
|         dryRun?: UIEventSource<boolean> | ||||
|         fakeUser?: false | boolean | ||||
|         oauth_token?: UIEventSource<string> | ||||
|         // Used to keep multiple changesets open and to write to the correct changeset
 | ||||
|                     singlePage?: boolean, | ||||
|                     osmConfiguration?: "osm" | "osm-test", | ||||
|         singlePage?: boolean | ||||
|         osmConfiguration?: "osm" | "osm-test" | ||||
|         attemptLogin?: true | boolean | ||||
|                 } | ||||
|     ) { | ||||
|         this.fakeUser = options.fakeUser ?? false; | ||||
|         this._singlePage = options.singlePage ?? true; | ||||
|         this._oauth_config = OsmConnection.oauth_configs[options.osmConfiguration ?? 'osm'] ?? OsmConnection.oauth_configs.osm; | ||||
|     }) { | ||||
|         this.fakeUser = options.fakeUser ?? false | ||||
|         this._singlePage = options.singlePage ?? true | ||||
|         this._oauth_config = | ||||
|             OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ?? | ||||
|             OsmConnection.oauth_configs.osm | ||||
|         console.debug("Using backend", this._oauth_config.url) | ||||
|         OsmObject.SetBackendUrl(this._oauth_config.url + "/") | ||||
|         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; | ||||
|         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top | ||||
| 
 | ||||
|         this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails"); | ||||
|         this.userDetails = new UIEventSource<UserDetails>( | ||||
|             new UserDetails(this._oauth_config.url), | ||||
|             "userDetails" | ||||
|         ) | ||||
|         if (options.fakeUser) { | ||||
|             const ud = this.userDetails.data; | ||||
|             const ud = this.userDetails.data | ||||
|             ud.csCount = 5678 | ||||
|             ud.loggedIn = true; | ||||
|             ud.loggedIn = true | ||||
|             ud.unreadMessages = 0 | ||||
|             ud.name = "Fake user" | ||||
|             ud.totalMessages = 42; | ||||
|             ud.totalMessages = 42 | ||||
|         } | ||||
|         const self = this; | ||||
|         this.isLoggedIn = this.userDetails.map(user => user.loggedIn); | ||||
|         this.isLoggedIn.addCallback(isLoggedIn => { | ||||
|         const self = this | ||||
|         this.isLoggedIn = this.userDetails.map((user) => user.loggedIn) | ||||
|         this.isLoggedIn.addCallback((isLoggedIn) => { | ||||
|             if (self.userDetails.data.loggedIn == false && isLoggedIn == true) { | ||||
|                 // We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do
 | ||||
|                 // This means someone attempted to toggle this; so we attempt to login!
 | ||||
|                 self.AttemptLogin() | ||||
|             } | ||||
|         }); | ||||
|         }) | ||||
| 
 | ||||
|         this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false); | ||||
|         this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false) | ||||
| 
 | ||||
|         this.updateAuthObject(); | ||||
|         this.updateAuthObject() | ||||
| 
 | ||||
|         this.preferencesHandler = new OsmPreferences(this.auth, this); | ||||
|         this.preferencesHandler = new OsmPreferences(this.auth, this) | ||||
| 
 | ||||
|         if (options.oauth_token?.data !== undefined) { | ||||
|             console.log(options.oauth_token.data) | ||||
|             const self = this; | ||||
|             this.auth.bootstrapToken(options.oauth_token.data, | ||||
|             const self = this | ||||
|             this.auth.bootstrapToken( | ||||
|                 options.oauth_token.data, | ||||
|                 (x) => { | ||||
|                     console.log("Called back: ", x) | ||||
|                     self.AttemptLogin(); | ||||
|                 }, this.auth); | ||||
| 
 | ||||
|             options.oauth_token.setData(undefined); | ||||
|                     self.AttemptLogin() | ||||
|                 }, | ||||
|                 this.auth | ||||
|             ) | ||||
| 
 | ||||
|             options.oauth_token.setData(undefined) | ||||
|         } | ||||
|         if (this.auth.authenticated() && (options.attemptLogin !== false)) { | ||||
|             this.AttemptLogin(); // Also updates the user badge
 | ||||
|         if (this.auth.authenticated() && options.attemptLogin !== false) { | ||||
|             this.AttemptLogin() // Also updates the user badge
 | ||||
|         } else { | ||||
|             console.log("Not authenticated"); | ||||
|             console.log("Not authenticated") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public CreateChangesetHandler(allElements: ElementStorage, changes: Changes){ | ||||
|         return new ChangesetHandler(this._dryRun, this, allElements, changes, this.auth); | ||||
|     public CreateChangesetHandler(allElements: ElementStorage, changes: Changes) { | ||||
|         return new ChangesetHandler(this._dryRun, this, allElements, changes, this.auth) | ||||
|     } | ||||
| 
 | ||||
|     public GetPreference(key: string, defaultValue: string = undefined, prefix: string = "mapcomplete-"): UIEventSource<string> { | ||||
|         return this.preferencesHandler.GetPreference(key, defaultValue, prefix); | ||||
|     public GetPreference( | ||||
|         key: string, | ||||
|         defaultValue: string = undefined, | ||||
|         prefix: string = "mapcomplete-" | ||||
|     ): UIEventSource<string> { | ||||
|         return this.preferencesHandler.GetPreference(key, defaultValue, prefix) | ||||
|     } | ||||
| 
 | ||||
|     public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { | ||||
|         return this.preferencesHandler.GetLongPreference(key, prefix); | ||||
|         return this.preferencesHandler.GetLongPreference(key, prefix) | ||||
|     } | ||||
| 
 | ||||
|     public OnLoggedIn(action: (userDetails: UserDetails) => void) { | ||||
|         this._onLoggedIn.push(action); | ||||
|         this._onLoggedIn.push(action) | ||||
|     } | ||||
| 
 | ||||
|     public LogOut() { | ||||
|         this.auth.logout(); | ||||
|         this.userDetails.data.loggedIn = false; | ||||
|         this.userDetails.data.csCount = 0; | ||||
|         this.userDetails.data.name = ""; | ||||
|         this.userDetails.ping(); | ||||
|         this.auth.logout() | ||||
|         this.userDetails.data.loggedIn = false | ||||
|         this.userDetails.data.csCount = 0 | ||||
|         this.userDetails.data.name = "" | ||||
|         this.userDetails.ping() | ||||
|         console.log("Logged out") | ||||
|         this.loadingStatus.setData("not-attempted") | ||||
|     } | ||||
| 
 | ||||
|     public Backend(): string { | ||||
|         return this._oauth_config.url; | ||||
|         return this._oauth_config.url | ||||
|     } | ||||
| 
 | ||||
|     public AttemptLogin() { | ||||
|  | @ -155,17 +163,19 @@ export class OsmConnection { | |||
|         if (this.fakeUser) { | ||||
|             this.loadingStatus.setData("logged-in") | ||||
|             console.log("AttemptLogin called, but ignored as fakeUser is set") | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         const self = this; | ||||
|         console.log("Trying to log in..."); | ||||
|         this.updateAuthObject(); | ||||
|         this.auth.xhr({ | ||||
|             method: 'GET', | ||||
|             path: '/api/0.6/user/details' | ||||
|         }, function (err, details) { | ||||
|         const self = this | ||||
|         console.log("Trying to log in...") | ||||
|         this.updateAuthObject() | ||||
|         this.auth.xhr( | ||||
|             { | ||||
|                 method: "GET", | ||||
|                 path: "/api/0.6/user/details", | ||||
|             }, | ||||
|             function (err, details) { | ||||
|                 if (err != null) { | ||||
|                 console.log(err); | ||||
|                     console.log(err) | ||||
|                     self.loadingStatus.setData("error") | ||||
|                     if (err.status == 401) { | ||||
|                         console.log("Clearing tokens...") | ||||
|  | @ -174,57 +184,60 @@ export class OsmConnection { | |||
|                         const tokens = [ | ||||
|                             "https://www.openstreetmap.orgoauth_request_token_secret", | ||||
|                             "https://www.openstreetmap.orgoauth_token", | ||||
|                         "https://www.openstreetmap.orgoauth_token_secret"] | ||||
|                     tokens.forEach(token => localStorage.removeItem(token)) | ||||
|                             "https://www.openstreetmap.orgoauth_token_secret", | ||||
|                         ] | ||||
|                         tokens.forEach((token) => localStorage.removeItem(token)) | ||||
|                     } | ||||
|                 return; | ||||
|                     return | ||||
|                 } | ||||
| 
 | ||||
|                 if (details == null) { | ||||
|                     self.loadingStatus.setData("error") | ||||
|                 return; | ||||
|                     return | ||||
|                 } | ||||
| 
 | ||||
|             self.CheckForMessagesContinuously(); | ||||
|                 self.CheckForMessagesContinuously() | ||||
| 
 | ||||
|                 // details is an XML DOM of user details
 | ||||
|             let userInfo = details.getElementsByTagName("user")[0]; | ||||
|                 let userInfo = details.getElementsByTagName("user")[0] | ||||
| 
 | ||||
|                 // let moreDetails = new DOMParser().parseFromString(userInfo.innerHTML, "text/xml");
 | ||||
| 
 | ||||
|             let data = self.userDetails.data; | ||||
|             data.loggedIn = true; | ||||
|             console.log("Login completed, userinfo is ", userInfo); | ||||
|             data.name = userInfo.getAttribute('display_name'); | ||||
|                 let data = self.userDetails.data | ||||
|                 data.loggedIn = true | ||||
|                 console.log("Login completed, userinfo is ", userInfo) | ||||
|                 data.name = userInfo.getAttribute("display_name") | ||||
|                 data.uid = Number(userInfo.getAttribute("id")) | ||||
|             data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count"); | ||||
|                 data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count") | ||||
| 
 | ||||
|             data.img = undefined; | ||||
|             const imgEl = userInfo.getElementsByTagName("img"); | ||||
|                 data.img = undefined | ||||
|                 const imgEl = userInfo.getElementsByTagName("img") | ||||
|                 if (imgEl !== undefined && imgEl[0] !== undefined) { | ||||
|                 data.img = imgEl[0].getAttribute("href"); | ||||
|                     data.img = imgEl[0].getAttribute("href") | ||||
|                 } | ||||
|             data.img = data.img ?? Img.AsData(Svg.osm_logo); | ||||
|                 data.img = data.img ?? Img.AsData(Svg.osm_logo) | ||||
| 
 | ||||
|             const homeEl = userInfo.getElementsByTagName("home"); | ||||
|                 const homeEl = userInfo.getElementsByTagName("home") | ||||
|                 if (homeEl !== undefined && homeEl[0] !== undefined) { | ||||
|                 const lat = parseFloat(homeEl[0].getAttribute("lat")); | ||||
|                 const lon = parseFloat(homeEl[0].getAttribute("lon")); | ||||
|                 data.home = {lat: lat, lon: lon}; | ||||
|                     const lat = parseFloat(homeEl[0].getAttribute("lat")) | ||||
|                     const lon = parseFloat(homeEl[0].getAttribute("lon")) | ||||
|                     data.home = { lat: lat, lon: lon } | ||||
|                 } | ||||
| 
 | ||||
|                 self.loadingStatus.setData("logged-in") | ||||
|             const messages = userInfo.getElementsByTagName("messages")[0].getElementsByTagName("received")[0]; | ||||
|             data.unreadMessages = parseInt(messages.getAttribute("unread")); | ||||
|             data.totalMessages = parseInt(messages.getAttribute("count")); | ||||
|                 const messages = userInfo | ||||
|                     .getElementsByTagName("messages")[0] | ||||
|                     .getElementsByTagName("received")[0] | ||||
|                 data.unreadMessages = parseInt(messages.getAttribute("unread")) | ||||
|                 data.totalMessages = parseInt(messages.getAttribute("count")) | ||||
| 
 | ||||
|             self.userDetails.ping(); | ||||
|                 self.userDetails.ping() | ||||
|                 for (const action of self._onLoggedIn) { | ||||
|                 action(self.userDetails.data); | ||||
|                     action(self.userDetails.data) | ||||
|                 } | ||||
|             self._onLoggedIn = []; | ||||
| 
 | ||||
|         }); | ||||
|                 self._onLoggedIn = [] | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     public closeNote(id: number | string, text?: string): Promise<void> { | ||||
|  | @ -236,22 +249,23 @@ export class OsmConnection { | |||
|             console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) | ||||
|             return new Promise((ok) => { | ||||
|                 ok() | ||||
|             }); | ||||
|             }) | ||||
|         } | ||||
|         return new Promise((ok, error) => { | ||||
|             this.auth.xhr({ | ||||
|                 method: 'POST', | ||||
|             this.auth.xhr( | ||||
|                 { | ||||
|                     method: "POST", | ||||
|                     path: `/api/0.6/notes/${id}/close${textSuffix}`, | ||||
|             }, function (err, _) { | ||||
|                 }, | ||||
|                 function (err, _) { | ||||
|                     if (err !== null) { | ||||
|                         error(err) | ||||
|                     } else { | ||||
|                         ok() | ||||
|                     } | ||||
|                 } | ||||
|             ) | ||||
|         }) | ||||
| 
 | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public reopenNote(id: number | string, text?: string): Promise<void> { | ||||
|  | @ -259,50 +273,52 @@ export class OsmConnection { | |||
|             console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) | ||||
|             return new Promise((ok) => { | ||||
|                 ok() | ||||
|             }); | ||||
|             }) | ||||
|         } | ||||
|         let textSuffix = "" | ||||
|         if ((text ?? "") !== "") { | ||||
|             textSuffix = "?text=" + encodeURIComponent(text) | ||||
|         } | ||||
|         return new Promise((ok, error) => { | ||||
|             this.auth.xhr({ | ||||
|                 method: 'POST', | ||||
|                 path: `/api/0.6/notes/${id}/reopen${textSuffix}` | ||||
|             }, function (err, _) { | ||||
|             this.auth.xhr( | ||||
|                 { | ||||
|                     method: "POST", | ||||
|                     path: `/api/0.6/notes/${id}/reopen${textSuffix}`, | ||||
|                 }, | ||||
|                 function (err, _) { | ||||
|                     if (err !== null) { | ||||
|                         error(err) | ||||
|                     } else { | ||||
|                         ok() | ||||
|                     } | ||||
|                 } | ||||
|             ) | ||||
|         }) | ||||
| 
 | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { | ||||
|         if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually opening note with text ", text) | ||||
|             return new Promise<{ id: number }>((ok) => { | ||||
|                 window.setTimeout(() => ok({id: Math.floor(Math.random() * 1000)}), Math.random() * 5000) | ||||
|             }); | ||||
|                 window.setTimeout( | ||||
|                     () => ok({ id: Math.floor(Math.random() * 1000) }), | ||||
|                     Math.random() * 5000 | ||||
|                 ) | ||||
|             }) | ||||
|         } | ||||
|         const auth = this.auth; | ||||
|         const content = {lat, lon, text} | ||||
|         const auth = this.auth | ||||
|         const content = { lat, lon, text } | ||||
|         return new Promise((ok, error) => { | ||||
|             auth.xhr({ | ||||
|                 method: 'POST', | ||||
|             auth.xhr( | ||||
|                 { | ||||
|                     method: "POST", | ||||
|                     path: `/api/0.6/notes.json`, | ||||
|                     options: { | ||||
|                     header: | ||||
|                         {'Content-Type': 'application/json'} | ||||
|                         header: { "Content-Type": "application/json" }, | ||||
|                     }, | ||||
|                 content: JSON.stringify(content) | ||||
| 
 | ||||
|             }, function ( | ||||
|                 err, | ||||
|                 response: string) { | ||||
|                     content: JSON.stringify(content), | ||||
|                 }, | ||||
|                 function (err, response: string) { | ||||
|                     console.log("RESPONSE IS", response) | ||||
|                     if (err !== null) { | ||||
|                         error(err) | ||||
|  | @ -310,12 +326,11 @@ export class OsmConnection { | |||
|                         const parsed = JSON.parse(response) | ||||
|                         const id = parsed.properties.id | ||||
|                         console.log("OPENED NOTE", id) | ||||
|                     ok({id}) | ||||
|                         ok({ id }) | ||||
|                     } | ||||
|                 } | ||||
|             ) | ||||
|         }) | ||||
| 
 | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public addCommentToNote(id: number | string, text: string): Promise<void> { | ||||
|  | @ -323,46 +338,53 @@ export class OsmConnection { | |||
|             console.warn("Dryrun enabled - not actually adding comment ", text, "to  note ", id) | ||||
|             return new Promise((ok) => { | ||||
|                 ok() | ||||
|             }); | ||||
|             }) | ||||
|         } | ||||
|         if ((text ?? "") === "") { | ||||
|             throw "Invalid text!" | ||||
|         } | ||||
| 
 | ||||
|         return new Promise((ok, error) => { | ||||
|             this.auth.xhr({ | ||||
|                 method: 'POST', | ||||
|             this.auth.xhr( | ||||
|                 { | ||||
|                     method: "POST", | ||||
| 
 | ||||
|                 path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}` | ||||
|             }, function (err, _) { | ||||
|                     path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`, | ||||
|                 }, | ||||
|                 function (err, _) { | ||||
|                     if (err !== null) { | ||||
|                         error(err) | ||||
|                     } else { | ||||
|                         ok() | ||||
|                     } | ||||
|                 } | ||||
|             ) | ||||
|         }) | ||||
| 
 | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private updateAuthObject() { | ||||
|         let pwaStandAloneMode = false; | ||||
|         let pwaStandAloneMode = false | ||||
|         try { | ||||
|             if (Utils.runningFromConsole) { | ||||
|                 pwaStandAloneMode = true | ||||
|             } else if (window.matchMedia('(display-mode: standalone)').matches || window.matchMedia('(display-mode: fullscreen)').matches) { | ||||
|                 pwaStandAloneMode = true; | ||||
|             } else if ( | ||||
|                 window.matchMedia("(display-mode: standalone)").matches || | ||||
|                 window.matchMedia("(display-mode: fullscreen)").matches | ||||
|             ) { | ||||
|                 pwaStandAloneMode = true | ||||
|             } | ||||
|         } catch (e) { | ||||
|             console.warn("Detecting standalone mode failed", e, ". Assuming in browser and not worrying furhter") | ||||
|             console.warn( | ||||
|                 "Detecting standalone mode failed", | ||||
|                 e, | ||||
|                 ". Assuming in browser and not worrying furhter" | ||||
|             ) | ||||
|         } | ||||
|         const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage; | ||||
|         const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage | ||||
| 
 | ||||
|         // In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
 | ||||
|         // Same for an iframe...
 | ||||
| 
 | ||||
| 
 | ||||
|         this.auth = new osmAuth({ | ||||
|             oauth_consumer_key: this._oauth_config.oauth_consumer_key, | ||||
|             oauth_secret: this._oauth_config.oauth_secret, | ||||
|  | @ -370,22 +392,20 @@ export class OsmConnection { | |||
|             landing: standalone ? undefined : window.location.href, | ||||
|             singlepage: !standalone, | ||||
|             auto: true, | ||||
| 
 | ||||
|         }); | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private CheckForMessagesContinuously() { | ||||
|         const self = this; | ||||
|         const self = this | ||||
|         if (this.isChecking) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         this.isChecking = true; | ||||
|         Stores.Chronic(5 * 60 * 1000).addCallback(_ => { | ||||
|         this.isChecking = true | ||||
|         Stores.Chronic(5 * 60 * 1000).addCallback((_) => { | ||||
|             if (self.isLoggedIn.data) { | ||||
|                 console.log("Checking for messages") | ||||
|                 self.AttemptLogin(); | ||||
|                 self.AttemptLogin() | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | @ -1,33 +1,32 @@ | |||
| import {Utils} from "../../Utils"; | ||||
| import * as polygon_features from "../../assets/polygon-features.json"; | ||||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import {BBox} from "../BBox"; | ||||
| import * as OsmToGeoJson from "osmtogeojson"; | ||||
| import {NodeId, OsmFeature, OsmId, OsmTags, RelationId, WayId} from "../../Models/OsmFeature"; | ||||
| import { Utils } from "../../Utils" | ||||
| import * as polygon_features from "../../assets/polygon-features.json" | ||||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import { BBox } from "../BBox" | ||||
| import * as OsmToGeoJson from "osmtogeojson" | ||||
| import { NodeId, OsmFeature, OsmId, OsmTags, RelationId, WayId } from "../../Models/OsmFeature" | ||||
| 
 | ||||
| export abstract class OsmObject { | ||||
| 
 | ||||
|     private static defaultBackend = "https://www.openstreetmap.org/" | ||||
|     protected static backendURL = OsmObject.defaultBackend; | ||||
|     protected static backendURL = OsmObject.defaultBackend | ||||
|     private static polygonFeatures = OsmObject.constructPolygonFeatures() | ||||
|     private static objectCache = new Map<string, UIEventSource<OsmObject>>(); | ||||
|     private static historyCache = new Map<string, UIEventSource<OsmObject[]>>(); | ||||
|     type: "node" | "way" | "relation"; | ||||
|     id: number; | ||||
|     private static objectCache = new Map<string, UIEventSource<OsmObject>>() | ||||
|     private static historyCache = new Map<string, UIEventSource<OsmObject[]>>() | ||||
|     type: "node" | "way" | "relation" | ||||
|     id: number | ||||
|     /** | ||||
|      * The OSM tags as simple object | ||||
|      */ | ||||
|     tags: OsmTags ; | ||||
|     version: number; | ||||
|     public changed: boolean = false; | ||||
|     timestamp: Date; | ||||
|     tags: OsmTags | ||||
|     version: number | ||||
|     public changed: boolean = false | ||||
|     timestamp: Date | ||||
| 
 | ||||
|     protected constructor(type: string, id: number) { | ||||
|         this.id = id; | ||||
|         this.id = id | ||||
|         // @ts-ignore
 | ||||
|         this.type = type; | ||||
|         this.type = type | ||||
|         this.tags = { | ||||
|             id: `${this.type}/${id}` | ||||
|             id: `${this.type}/${id}`, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -38,63 +37,63 @@ export abstract class OsmObject { | |||
|         if (!url.startsWith("http")) { | ||||
|             throw "Backend URL must begin with http" | ||||
|         } | ||||
|         this.backendURL = url; | ||||
|         this.backendURL = url | ||||
|     } | ||||
| 
 | ||||
|     public static DownloadObject(id: string, forceRefresh: boolean = false): Store<OsmObject> { | ||||
|         let src: UIEventSource<OsmObject>; | ||||
|         let src: UIEventSource<OsmObject> | ||||
|         if (OsmObject.objectCache.has(id)) { | ||||
|             src = OsmObject.objectCache.get(id) | ||||
|             if (forceRefresh) { | ||||
|                 src.setData(undefined) | ||||
|             } else { | ||||
|                 return src; | ||||
|                 return src | ||||
|             } | ||||
|         } else { | ||||
|             src = UIEventSource.FromPromise(OsmObject.DownloadObjectAsync(id)) | ||||
|         } | ||||
| 
 | ||||
|         OsmObject.objectCache.set(id, src); | ||||
|         return src; | ||||
|         OsmObject.objectCache.set(id, src) | ||||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     static async DownloadPropertiesOf(id: string): Promise<any> { | ||||
|         const splitted = id.split("/"); | ||||
|         const idN = Number(splitted[1]); | ||||
|         const splitted = id.split("/") | ||||
|         const idN = Number(splitted[1]) | ||||
|         if (idN < 0) { | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         const url = `${OsmObject.backendURL}api/0.6/${id}`; | ||||
|         const url = `${OsmObject.backendURL}api/0.6/${id}` | ||||
|         const rawData = await Utils.downloadJsonCached(url, 1000) | ||||
|         return rawData.elements[0].tags | ||||
|     } | ||||
| 
 | ||||
|     static async DownloadObjectAsync(id: NodeId): Promise<OsmNode | undefined>; | ||||
|     static async DownloadObjectAsync(id: WayId): Promise<OsmWay | undefined>; | ||||
|     static async DownloadObjectAsync(id: RelationId): Promise<OsmRelation | undefined>; | ||||
|     static async DownloadObjectAsync(id: OsmId): Promise<OsmObject | undefined>; | ||||
|     static async DownloadObjectAsync(id: string): Promise<OsmObject | undefined>; | ||||
|     static async DownloadObjectAsync(id: string): Promise<OsmObject | undefined>{ | ||||
|         const splitted = id.split("/"); | ||||
|         const type = splitted[0]; | ||||
|         const idN = Number(splitted[1]); | ||||
|     static async DownloadObjectAsync(id: NodeId): Promise<OsmNode | undefined> | ||||
|     static async DownloadObjectAsync(id: WayId): Promise<OsmWay | undefined> | ||||
|     static async DownloadObjectAsync(id: RelationId): Promise<OsmRelation | undefined> | ||||
|     static async DownloadObjectAsync(id: OsmId): Promise<OsmObject | undefined> | ||||
|     static async DownloadObjectAsync(id: string): Promise<OsmObject | undefined> | ||||
|     static async DownloadObjectAsync(id: string): Promise<OsmObject | undefined> { | ||||
|         const splitted = id.split("/") | ||||
|         const type = splitted[0] | ||||
|         const idN = Number(splitted[1]) | ||||
|         if (idN < 0) { | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         const full = (!id.startsWith("node")) ? "/full" : ""; | ||||
|         const url = `${OsmObject.backendURL}api/0.6/${id}${full}`; | ||||
|         const full = !id.startsWith("node") ? "/full" : "" | ||||
|         const url = `${OsmObject.backendURL}api/0.6/${id}${full}` | ||||
|         const rawData = await Utils.downloadJsonCached(url, 10000) | ||||
|         if (rawData === undefined) { | ||||
|             return undefined | ||||
|         } | ||||
|         // A full query might contain more then just the requested object (e.g. nodes that are part of a way, where we only want the way)
 | ||||
|         const parsed = OsmObject.ParseObjects(rawData.elements); | ||||
|         const parsed = OsmObject.ParseObjects(rawData.elements) | ||||
|         // Lets fetch the object we need
 | ||||
|         for (const osmObject of parsed) { | ||||
|             if (osmObject.type !== type) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
|             if (osmObject.id !== idN) { | ||||
|                 continue | ||||
|  | @ -103,25 +102,23 @@ export abstract class OsmObject { | |||
|             return osmObject | ||||
|         } | ||||
|         throw "PANIC: requested object is not part of the response" | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Downloads the ways that are using this node. | ||||
|      * Beware: their geometry will be incomplete! | ||||
|      */ | ||||
|     public static DownloadReferencingWays(id: string): Promise<OsmWay[]> { | ||||
|         return Utils.downloadJsonCached(`${OsmObject.backendURL}api/0.6/${id}/ways`, 60 * 1000).then( | ||||
|             data => { | ||||
|                 return data.elements.map(wayInfo => { | ||||
|         return Utils.downloadJsonCached( | ||||
|             `${OsmObject.backendURL}api/0.6/${id}/ways`, | ||||
|             60 * 1000 | ||||
|         ).then((data) => { | ||||
|             return data.elements.map((wayInfo) => { | ||||
|                 const way = new OsmWay(wayInfo.id) | ||||
|                 way.LoadData(wayInfo) | ||||
|                 return way | ||||
|             }) | ||||
|             } | ||||
|         ) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -129,8 +126,11 @@ export abstract class OsmObject { | |||
|      * Beware: their geometry will be incomplete! | ||||
|      */ | ||||
|     public static async DownloadReferencingRelations(id: string): Promise<OsmRelation[]> { | ||||
|         const data = await Utils.downloadJsonCached(`${OsmObject.backendURL}api/0.6/${id}/relations`, 60 * 1000) | ||||
|         return data.elements.map(wayInfo => { | ||||
|         const data = await Utils.downloadJsonCached( | ||||
|             `${OsmObject.backendURL}api/0.6/${id}/relations`, | ||||
|             60 * 1000 | ||||
|         ) | ||||
|         return data.elements.map((wayInfo) => { | ||||
|             const rel = new OsmRelation(wayInfo.id) | ||||
|             rel.LoadData(wayInfo) | ||||
|             rel.SaveExtraData(wayInfo, undefined) | ||||
|  | @ -138,78 +138,85 @@ export abstract class OsmObject { | |||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public static DownloadHistory(id: string): UIEventSource<OsmObject []> { | ||||
|     public static DownloadHistory(id: string): UIEventSource<OsmObject[]> { | ||||
|         if (OsmObject.historyCache.has(id)) { | ||||
|             return OsmObject.historyCache.get(id) | ||||
|         } | ||||
|         const splitted = id.split("/"); | ||||
|         const type = splitted[0]; | ||||
|         const idN = Number(splitted[1]); | ||||
|         const src = new UIEventSource<OsmObject[]>([]); | ||||
|         OsmObject.historyCache.set(id, src); | ||||
|         Utils.downloadJsonCached(`${OsmObject.backendURL}api/0.6/${type}/${idN}/history`, 10 * 60 * 1000).then(data => { | ||||
|             const elements: any[] = data.elements; | ||||
|         const splitted = id.split("/") | ||||
|         const type = splitted[0] | ||||
|         const idN = Number(splitted[1]) | ||||
|         const src = new UIEventSource<OsmObject[]>([]) | ||||
|         OsmObject.historyCache.set(id, src) | ||||
|         Utils.downloadJsonCached( | ||||
|             `${OsmObject.backendURL}api/0.6/${type}/${idN}/history`, | ||||
|             10 * 60 * 1000 | ||||
|         ).then((data) => { | ||||
|             const elements: any[] = data.elements | ||||
|             const osmObjects: OsmObject[] = [] | ||||
|             for (const element of elements) { | ||||
|                 let osmObject: OsmObject = null | ||||
|                 switch (type) { | ||||
|                     case("node"): | ||||
|                         osmObject = new OsmNode(idN); | ||||
|                         break; | ||||
|                     case("way"): | ||||
|                         osmObject = new OsmWay(idN); | ||||
|                         break; | ||||
|                     case("relation"): | ||||
|                         osmObject = new OsmRelation(idN); | ||||
|                         break; | ||||
|                     case "node": | ||||
|                         osmObject = new OsmNode(idN) | ||||
|                         break | ||||
|                     case "way": | ||||
|                         osmObject = new OsmWay(idN) | ||||
|                         break | ||||
|                     case "relation": | ||||
|                         osmObject = new OsmRelation(idN) | ||||
|                         break | ||||
|                 } | ||||
|                 osmObject?.LoadData(element); | ||||
|                 osmObject?.SaveExtraData(element, []); | ||||
|                 osmObject?.LoadData(element) | ||||
|                 osmObject?.SaveExtraData(element, []) | ||||
|                 osmObjects.push(osmObject) | ||||
|             } | ||||
|             src.setData(osmObjects) | ||||
|         }) | ||||
|         return src; | ||||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     // bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
 | ||||
|     public static async LoadArea(bbox: BBox): Promise<OsmObject[]> { | ||||
|         const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` | ||||
|         const data = await Utils.downloadJson(url) | ||||
|         const elements: any[] = data.elements; | ||||
|         return OsmObject.ParseObjects(elements); | ||||
|         const elements: any[] = data.elements | ||||
|         return OsmObject.ParseObjects(elements) | ||||
|     } | ||||
| 
 | ||||
|     public static ParseObjects(elements: any[]): OsmObject[] { | ||||
|         const objects: OsmObject[] = []; | ||||
|         const objects: OsmObject[] = [] | ||||
|         const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>() | ||||
| 
 | ||||
|         for (const element of elements) { | ||||
|             const type = element.type; | ||||
|             const idN = element.id; | ||||
|             const type = element.type | ||||
|             const idN = element.id | ||||
|             let osmObject: OsmObject = null | ||||
|             switch (type) { | ||||
|                 case("node"): | ||||
|                     const node = new OsmNode(idN); | ||||
|                     allNodes.set(idN, node); | ||||
|                 case "node": | ||||
|                     const node = new OsmNode(idN) | ||||
|                     allNodes.set(idN, node) | ||||
|                     osmObject = node | ||||
|                     node.SaveExtraData(element); | ||||
|                     break; | ||||
|                 case("way"): | ||||
|                     osmObject = new OsmWay(idN); | ||||
|                     const nodes = element.nodes.map(i => allNodes.get(i)); | ||||
|                     node.SaveExtraData(element) | ||||
|                     break | ||||
|                 case "way": | ||||
|                     osmObject = new OsmWay(idN) | ||||
|                     const nodes = element.nodes.map((i) => allNodes.get(i)) | ||||
|                     osmObject.SaveExtraData(element, nodes) | ||||
|                     break; | ||||
|                 case("relation"): | ||||
|                     osmObject = new OsmRelation(idN); | ||||
|                     const allGeojsons = OsmToGeoJson.default({elements}, | ||||
|                     break | ||||
|                 case "relation": | ||||
|                     osmObject = new OsmRelation(idN) | ||||
|                     const allGeojsons = OsmToGeoJson.default( | ||||
|                         { elements }, | ||||
|                         // @ts-ignore
 | ||||
|                         { | ||||
|                             flatProperties: true | ||||
|                         }); | ||||
|                     const feature = allGeojsons.features.find(f => f.id === osmObject.type + "/" + osmObject.id) | ||||
|                             flatProperties: true, | ||||
|                         } | ||||
|                     ) | ||||
|                     const feature = allGeojsons.features.find( | ||||
|                         (f) => f.id === osmObject.type + "/" + osmObject.id | ||||
|                     ) | ||||
|                     osmObject.SaveExtraData(element, feature) | ||||
|                     break; | ||||
|                     break | ||||
|             } | ||||
| 
 | ||||
|             if (osmObject !== undefined && OsmObject.backendURL !== OsmObject.defaultBackend) { | ||||
|  | @ -219,7 +226,7 @@ export abstract class OsmObject { | |||
|             osmObject?.LoadData(element) | ||||
|             objects.push(osmObject) | ||||
|         } | ||||
|         return objects; | ||||
|         return objects | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -233,11 +240,12 @@ export abstract class OsmObject { | |||
|             if (!tags.hasOwnProperty(tagsKey)) { | ||||
|                 continue | ||||
|             } | ||||
|             const polyGuide: { values: Set<string>; blacklist: boolean } = OsmObject.polygonFeatures.get(tagsKey) | ||||
|             const polyGuide: { values: Set<string>; blacklist: boolean } = | ||||
|                 OsmObject.polygonFeatures.get(tagsKey) | ||||
|             if (polyGuide === undefined) { | ||||
|                 continue | ||||
|             } | ||||
|             if ((polyGuide.values === null)) { | ||||
|             if (polyGuide.values === null) { | ||||
|                 // .values is null, thus merely _having_ this key is enough to be a polygon (or if blacklist, being a line)
 | ||||
|                 return !polyGuide.blacklist | ||||
|             } | ||||
|  | @ -249,156 +257,178 @@ export abstract class OsmObject { | |||
|             return doesMatch | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     private static constructPolygonFeatures(): Map<string, { values: Set<string>, blacklist: boolean }> { | ||||
|         const result = new Map<string, { values: Set<string>, blacklist: boolean }>(); | ||||
|         for (const polygonFeature of (polygon_features["default"] ?? polygon_features)) { | ||||
|             const key = polygonFeature.key; | ||||
|     private static constructPolygonFeatures(): Map< | ||||
|         string, | ||||
|         { values: Set<string>; blacklist: boolean } | ||||
|     > { | ||||
|         const result = new Map<string, { values: Set<string>; blacklist: boolean }>() | ||||
|         for (const polygonFeature of polygon_features["default"] ?? polygon_features) { | ||||
|             const key = polygonFeature.key | ||||
| 
 | ||||
|             if (polygonFeature.polygon === "all") { | ||||
|                 result.set(key, {values: null, blacklist: false}) | ||||
|                 result.set(key, { values: null, blacklist: false }) | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             const blacklist = polygonFeature.polygon === "blacklist" | ||||
|             result.set(key, {values: new Set<string>(polygonFeature.values), blacklist: blacklist}) | ||||
| 
 | ||||
|             result.set(key, { | ||||
|                 values: new Set<string>(polygonFeature.values), | ||||
|                 blacklist: blacklist, | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         return result; | ||||
|         return result | ||||
|     } | ||||
| 
 | ||||
|     // The centerpoint of the feature, as [lat, lon]
 | ||||
|     public abstract centerpoint(): [number, number]; | ||||
|     public abstract centerpoint(): [number, number] | ||||
| 
 | ||||
|     public abstract asGeoJson(): any; | ||||
|     public abstract asGeoJson(): any | ||||
| 
 | ||||
|     abstract SaveExtraData(element: any, allElements: OsmObject[] | any); | ||||
|     abstract SaveExtraData(element: any, allElements: OsmObject[] | any) | ||||
| 
 | ||||
|     /** | ||||
|      * Generates the changeset-XML for tags | ||||
|      * @constructor | ||||
|      */ | ||||
|     TagsXML(): string { | ||||
|         let tags = ""; | ||||
|         let tags = "" | ||||
|         for (const key in this.tags) { | ||||
|             if (key.startsWith("_")) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
|             if (key === "id") { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
|             const v = this.tags[key]; | ||||
|             const v = this.tags[key] | ||||
|             if (v !== "" && v !== undefined) { | ||||
|                 tags += '        <tag k="' + Utils.EncodeXmlValue(key) + '" v="' + Utils.EncodeXmlValue(this.tags[key]) + '"/>\n' | ||||
|                 tags += | ||||
|                     '        <tag k="' + | ||||
|                     Utils.EncodeXmlValue(key) + | ||||
|                     '" v="' + | ||||
|                     Utils.EncodeXmlValue(this.tags[key]) + | ||||
|                     '"/>\n' | ||||
|             } | ||||
|         } | ||||
|         return tags; | ||||
|         return tags | ||||
|     } | ||||
| 
 | ||||
|     abstract ChangesetXML(changesetId: string): string; | ||||
|     abstract ChangesetXML(changesetId: string): string | ||||
| 
 | ||||
|     protected VersionXML() { | ||||
|         if (this.version === undefined) { | ||||
|             return ""; | ||||
|             return "" | ||||
|         } | ||||
|         return 'version="' + this.version + '"'; | ||||
|         return 'version="' + this.version + '"' | ||||
|     } | ||||
| 
 | ||||
|     private LoadData(element: any): void { | ||||
|         this.tags = element.tags ?? this.tags; | ||||
|         this.version = element.version; | ||||
|         this.timestamp = element.timestamp; | ||||
|         const tgs = this.tags; | ||||
|         this.tags = element.tags ?? this.tags | ||||
|         this.version = element.version | ||||
|         this.timestamp = element.timestamp | ||||
|         const tgs = this.tags | ||||
|         if (element.tags === undefined) { | ||||
|             // Simple node which is part of a way - not important
 | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         tgs["_last_edit:contributor"] = element.user | ||||
|         tgs["_last_edit:contributor:uid"] = element.uid | ||||
|         tgs["_last_edit:changeset"] = element.changeset | ||||
|         tgs["_last_edit:timestamp"] = element.timestamp | ||||
|         tgs["_version_number"] = element.version | ||||
|         tgs["id"] =<OsmId> ( this.type + "/" + this.id); | ||||
|         tgs["id"] = <OsmId>(this.type + "/" + this.id) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export class OsmNode extends OsmObject { | ||||
| 
 | ||||
|     lat: number; | ||||
|     lon: number; | ||||
|     lat: number | ||||
|     lon: number | ||||
| 
 | ||||
|     constructor(id: number) { | ||||
|         super("node", id); | ||||
| 
 | ||||
|         super("node", id) | ||||
|     } | ||||
| 
 | ||||
|     ChangesetXML(changesetId: string): string { | ||||
|         let tags = this.TagsXML(); | ||||
|         let tags = this.TagsXML() | ||||
| 
 | ||||
|         return '    <node id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + ' lat="' + this.lat + '" lon="' + this.lon + '">\n' + | ||||
|         return ( | ||||
|             '    <node id="' + | ||||
|             this.id + | ||||
|             '" changeset="' + | ||||
|             changesetId + | ||||
|             '" ' + | ||||
|             this.VersionXML() + | ||||
|             ' lat="' + | ||||
|             this.lat + | ||||
|             '" lon="' + | ||||
|             this.lon + | ||||
|             '">\n' + | ||||
|             tags + | ||||
|             '    </node>\n'; | ||||
|             "    </node>\n" | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     SaveExtraData(element) { | ||||
|         this.lat = element.lat; | ||||
|         this.lon = element.lon; | ||||
|         this.lat = element.lat | ||||
|         this.lon = element.lon | ||||
|     } | ||||
| 
 | ||||
|     centerpoint(): [number, number] { | ||||
|         return [this.lat, this.lon]; | ||||
|         return [this.lat, this.lon] | ||||
|     } | ||||
| 
 | ||||
|     asGeoJson() : OsmFeature{ | ||||
|     asGeoJson(): OsmFeature { | ||||
|         return { | ||||
|             "type": "Feature", | ||||
|             "properties": this.tags, | ||||
|             "geometry": { | ||||
|                 "type": "Point", | ||||
|                 "coordinates": [ | ||||
|                     this.lon, | ||||
|                     this.lat | ||||
|                 ] | ||||
|             } | ||||
|             type: "Feature", | ||||
|             properties: this.tags, | ||||
|             geometry: { | ||||
|                 type: "Point", | ||||
|                 coordinates: [this.lon, this.lat], | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class OsmWay extends OsmObject { | ||||
| 
 | ||||
|     nodes: number[] = []; | ||||
|     nodes: number[] = [] | ||||
|     // The coordinates of the way, [lat, lon][]
 | ||||
|     coordinates: [number, number][] = [] | ||||
|     lat: number; | ||||
|     lon: number; | ||||
|     lat: number | ||||
|     lon: number | ||||
| 
 | ||||
|     constructor(id: number) { | ||||
|         super("way", id); | ||||
|         super("way", id) | ||||
|     } | ||||
| 
 | ||||
|     centerpoint(): [number, number] { | ||||
|         return [this.lat, this.lon]; | ||||
|         return [this.lat, this.lon] | ||||
|     } | ||||
| 
 | ||||
|     ChangesetXML(changesetId: string): string { | ||||
|         let tags = this.TagsXML(); | ||||
|         let nds = ""; | ||||
|         let tags = this.TagsXML() | ||||
|         let nds = "" | ||||
|         for (const node in this.nodes) { | ||||
|             nds += '      <nd ref="' + this.nodes[node] + '"/>\n'; | ||||
|             nds += '      <nd ref="' + this.nodes[node] + '"/>\n' | ||||
|         } | ||||
| 
 | ||||
|         return '    <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' + | ||||
|         return ( | ||||
|             '    <way id="' + | ||||
|             this.id + | ||||
|             '" changeset="' + | ||||
|             changesetId + | ||||
|             '" ' + | ||||
|             this.VersionXML() + | ||||
|             ">\n" + | ||||
|             nds + | ||||
|             tags + | ||||
|             '    </way>\n'; | ||||
|             "    </way>\n" | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     SaveExtraData(element, allNodes: OsmNode[]) { | ||||
| 
 | ||||
|         let latSum = 0 | ||||
|         let lonSum = 0 | ||||
| 
 | ||||
|  | @ -416,87 +446,95 @@ export class OsmWay extends OsmObject { | |||
|             if (node === undefined) { | ||||
|                 console.error("Error: node ", nodeId, "not found in ", nodeDict) | ||||
|                 // This is probably part of a relation which hasn't been fully downloaded
 | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
|             this.coordinates.push(node.centerpoint()); | ||||
|             this.coordinates.push(node.centerpoint()) | ||||
|             latSum += node.lat | ||||
|             lonSum += node.lon | ||||
|         } | ||||
|         let count = this.coordinates.length; | ||||
|         this.lat = latSum / count; | ||||
|         this.lon = lonSum / count; | ||||
|         this.nodes = element.nodes; | ||||
|         let count = this.coordinates.length | ||||
|         this.lat = latSum / count | ||||
|         this.lon = lonSum / count | ||||
|         this.nodes = element.nodes | ||||
|     } | ||||
| 
 | ||||
|     public asGeoJson() { | ||||
|         let coordinates: ([number, number][] | [number, number][][]) = this.coordinates.map(([lat, lon]) => [lon, lat]); | ||||
|         let coordinates: [number, number][] | [number, number][][] = this.coordinates.map( | ||||
|             ([lat, lon]) => [lon, lat] | ||||
|         ) | ||||
|         if (this.isPolygon()) { | ||||
|             coordinates = [coordinates] | ||||
|         } | ||||
|         return { | ||||
|             "type": "Feature", | ||||
|             "properties": this.tags, | ||||
|             "geometry": { | ||||
|                 "type": this.isPolygon() ? "Polygon" : "LineString", | ||||
|                 "coordinates": coordinates | ||||
|             } | ||||
|             type: "Feature", | ||||
|             properties: this.tags, | ||||
|             geometry: { | ||||
|                 type: this.isPolygon() ? "Polygon" : "LineString", | ||||
|                 coordinates: coordinates, | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private isPolygon(): boolean { | ||||
|         // Compare lat and lon seperately, as the coordinate array might not be a reference to the same object
 | ||||
|         if (this.coordinates[0][0] !== this.coordinates[this.coordinates.length - 1][0] || | ||||
|             this.coordinates[0][1] !== this.coordinates[this.coordinates.length - 1][1]) { | ||||
|             return false; // Not closed
 | ||||
|         if ( | ||||
|             this.coordinates[0][0] !== this.coordinates[this.coordinates.length - 1][0] || | ||||
|             this.coordinates[0][1] !== this.coordinates[this.coordinates.length - 1][1] | ||||
|         ) { | ||||
|             return false // Not closed
 | ||||
|         } | ||||
|         return OsmObject.isPolygon(this.tags) | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class OsmRelation extends OsmObject { | ||||
| 
 | ||||
|     public members: { | ||||
|         type: "node" | "way" | "relation", | ||||
|         ref: number, | ||||
|         type: "node" | "way" | "relation" | ||||
|         ref: number | ||||
|         role: string | ||||
|     }[]; | ||||
|     }[] | ||||
| 
 | ||||
|     private geojson = undefined | ||||
| 
 | ||||
|     constructor(id: number) { | ||||
|         super("relation", id); | ||||
|         super("relation", id) | ||||
|     } | ||||
| 
 | ||||
|     centerpoint(): [number, number] { | ||||
|         return [0, 0]; // TODO
 | ||||
|         return [0, 0] // TODO
 | ||||
|     } | ||||
| 
 | ||||
|     ChangesetXML(changesetId: string): string { | ||||
|         let members = ""; | ||||
|         let members = "" | ||||
|         for (const member of this.members) { | ||||
|             members += '      <member type="' + member.type + '" ref="' + member.ref + '" role="' + member.role + '"/>\n'; | ||||
|             members += | ||||
|                 '      <member type="' + | ||||
|                 member.type + | ||||
|                 '" ref="' + | ||||
|                 member.ref + | ||||
|                 '" role="' + | ||||
|                 member.role + | ||||
|                 '"/>\n' | ||||
|         } | ||||
| 
 | ||||
|         let tags = this.TagsXML(); | ||||
|         let tags = this.TagsXML() | ||||
|         let cs = "" | ||||
|         if (changesetId !== undefined) { | ||||
|             cs = `changeset="${changesetId}"` | ||||
|         } | ||||
|         return `    <relation id="${this.id}" ${cs} ${this.VersionXML()}>
 | ||||
| ${members}${tags}        </relation> | ||||
| `;
 | ||||
| 
 | ||||
| ` | ||||
|     } | ||||
| 
 | ||||
|     SaveExtraData(element, geojson) { | ||||
|         this.members = element.members; | ||||
|         this.members = element.members | ||||
|         this.geojson = geojson | ||||
|     } | ||||
| 
 | ||||
|     asGeoJson(): any { | ||||
|         if (this.geojson !== undefined) { | ||||
|             return this.geojson; | ||||
|             return this.geojson | ||||
|         } | ||||
|         throw "Not Implemented" | ||||
|     } | ||||
|  |  | |||
|  | @ -1,22 +1,21 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import UserDetails, {OsmConnection} from "./OsmConnection"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {DomEvent} from "leaflet"; | ||||
| import preventDefault = DomEvent.preventDefault; | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import UserDetails, { OsmConnection } from "./OsmConnection" | ||||
| import { Utils } from "../../Utils" | ||||
| import { DomEvent } from "leaflet" | ||||
| import preventDefault = DomEvent.preventDefault | ||||
| 
 | ||||
| export class OsmPreferences { | ||||
| 
 | ||||
|     public preferences = new UIEventSource<Record<string, string>>({}, "all-osm-preferences"); | ||||
|     public preferences = new UIEventSource<Record<string, string>>({}, "all-osm-preferences") | ||||
|     private readonly preferenceSources = new Map<string, UIEventSource<string>>() | ||||
|     private auth: any; | ||||
|     private userDetails: UIEventSource<UserDetails>; | ||||
|     private longPreferences = {}; | ||||
|     private auth: any | ||||
|     private userDetails: UIEventSource<UserDetails> | ||||
|     private longPreferences = {} | ||||
| 
 | ||||
|     constructor(auth, osmConnection: OsmConnection) { | ||||
|         this.auth = auth; | ||||
|         this.userDetails = osmConnection.userDetails; | ||||
|         const self = this; | ||||
|         osmConnection.OnLoggedIn(() => self.UpdatePreferences()); | ||||
|         this.auth = auth | ||||
|         this.userDetails = osmConnection.userDetails | ||||
|         const self = this | ||||
|         osmConnection.OnLoggedIn(() => self.UpdatePreferences()) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -26,42 +25,44 @@ export class OsmPreferences { | |||
|      * @constructor | ||||
|      */ | ||||
|     public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { | ||||
| 
 | ||||
|         if (this.longPreferences[prefix + key] !== undefined) { | ||||
|             return this.longPreferences[prefix + key]; | ||||
|             return this.longPreferences[prefix + key] | ||||
|         } | ||||
| 
 | ||||
|         const source = new UIEventSource<string>(undefined, "long-osm-preference:" + prefix + key) | ||||
|         this.longPreferences[prefix + key] = source | ||||
| 
 | ||||
|         const source = new UIEventSource<string>(undefined, "long-osm-preference:" + prefix + key); | ||||
|         this.longPreferences[prefix + key] = source; | ||||
| 
 | ||||
|         const allStartWith = prefix + key + "-combined"; | ||||
|         const allStartWith = prefix + key + "-combined" | ||||
|         // Gives the number of combined preferences
 | ||||
|         const length = this.GetPreference(allStartWith + "-length", "", ""); | ||||
|         const length = this.GetPreference(allStartWith + "-length", "", "") | ||||
| 
 | ||||
|        if( (allStartWith + "-length").length > 255){ | ||||
|            throw "This preference key is too long, it has "+key.length+" characters, but at most "+(255 - "-length".length - "-combined".length - prefix.length)+" characters are allowed" | ||||
|         if ((allStartWith + "-length").length > 255) { | ||||
|             throw ( | ||||
|                 "This preference key is too long, it has " + | ||||
|                 key.length + | ||||
|                 " characters, but at most " + | ||||
|                 (255 - "-length".length - "-combined".length - prefix.length) + | ||||
|                 " characters are allowed" | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         const self = this; | ||||
|         source.addCallback(str => { | ||||
|         const self = this | ||||
|         source.addCallback((str) => { | ||||
|             if (str === undefined || str === "") { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             if (str === null) { | ||||
|                 console.error("Deleting " + allStartWith); | ||||
|                 let count = parseInt(length.data); | ||||
|                 console.error("Deleting " + allStartWith) | ||||
|                 let count = parseInt(length.data) | ||||
|                 for (let i = 0; i < count; i++) { | ||||
|                     // Delete all the preferences
 | ||||
|                     self.GetPreference(allStartWith + "-" + i, "", "") | ||||
|                         .setData(""); | ||||
|                     self.GetPreference(allStartWith + "-" + i, "", "").setData("") | ||||
|                 } | ||||
|                 self.GetPreference(allStartWith + "-length", "", "") | ||||
|                     .setData(""); | ||||
|                 self.GetPreference(allStartWith + "-length", "", "").setData("") | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             let i = 0; | ||||
|             let i = 0 | ||||
|             while (str !== "") { | ||||
|                 if (str === undefined || str === "undefined") { | ||||
|                     throw "Long pref became undefined?" | ||||
|  | @ -69,79 +70,91 @@ export class OsmPreferences { | |||
|                 if (i > 100) { | ||||
|                     throw "This long preference is getting very long... " | ||||
|                 } | ||||
|                 self.GetPreference(allStartWith + "-" + i, "","").setData(str.substr(0, 255)); | ||||
|                 str = str.substr(255); | ||||
|                 i++; | ||||
|                 self.GetPreference(allStartWith + "-" + i, "", "").setData(str.substr(0, 255)) | ||||
|                 str = str.substr(255) | ||||
|                 i++ | ||||
|             } | ||||
|             length.setData("" + i); // We use I, the number of preference fields used
 | ||||
|         }); | ||||
| 
 | ||||
|             length.setData("" + i) // We use I, the number of preference fields used
 | ||||
|         }) | ||||
| 
 | ||||
|         function updateData(l: number) { | ||||
|             if(Object.keys(self.preferences.data).length === 0){ | ||||
|             if (Object.keys(self.preferences.data).length === 0) { | ||||
|                 // The preferences are still empty - they are not yet updated, so we delay updating for now
 | ||||
|                 return | ||||
|             } | ||||
|             const prefsCount = Number(l); | ||||
|             const prefsCount = Number(l) | ||||
|             if (prefsCount > 100) { | ||||
|                 throw "Length to long"; | ||||
|                 throw "Length to long" | ||||
|             } | ||||
|             let str = ""; | ||||
|             let str = "" | ||||
|             for (let i = 0; i < prefsCount; i++) { | ||||
|                 const key = allStartWith + "-" + i | ||||
|                 if(self.preferences.data[key] === undefined){ | ||||
|                     console.warn("Detected a broken combined preference:", key, "is undefined", self.preferences) | ||||
|                 if (self.preferences.data[key] === undefined) { | ||||
|                     console.warn( | ||||
|                         "Detected a broken combined preference:", | ||||
|                         key, | ||||
|                         "is undefined", | ||||
|                         self.preferences | ||||
|                     ) | ||||
|                 } | ||||
|                 str += self.preferences.data[key] ?? ""; | ||||
|                 str += self.preferences.data[key] ?? "" | ||||
|             } | ||||
| 
 | ||||
|             source.setData(str); | ||||
|             source.setData(str) | ||||
|         } | ||||
| 
 | ||||
|         length.addCallback(l => { | ||||
|             updateData(Number(l)); | ||||
|         }); | ||||
|         this.preferences.addCallbackAndRun(_ => { | ||||
|             updateData(Number(length.data)); | ||||
|         length.addCallback((l) => { | ||||
|             updateData(Number(l)) | ||||
|         }) | ||||
|         this.preferences.addCallbackAndRun((_) => { | ||||
|             updateData(Number(length.data)) | ||||
|         }) | ||||
| 
 | ||||
|         return source; | ||||
|         return source | ||||
|     } | ||||
| 
 | ||||
|     public GetPreference(key: string, defaultValue : string = undefined, prefix: string = "mapcomplete-"): UIEventSource<string> { | ||||
|         if(key.startsWith(prefix) && prefix !== ""){ | ||||
|             console.trace("A preference was requested which has a duplicate prefix in its key. This is probably a bug") | ||||
|     public GetPreference( | ||||
|         key: string, | ||||
|         defaultValue: string = undefined, | ||||
|         prefix: string = "mapcomplete-" | ||||
|     ): UIEventSource<string> { | ||||
|         if (key.startsWith(prefix) && prefix !== "") { | ||||
|             console.trace( | ||||
|                 "A preference was requested which has a duplicate prefix in its key. This is probably a bug" | ||||
|             ) | ||||
|         } | ||||
|         key = prefix + key; | ||||
|         key = key.replace(/[:\\\/"' {}.%]/g, '') | ||||
|         key = prefix + key | ||||
|         key = key.replace(/[:\\\/"' {}.%]/g, "") | ||||
|         if (key.length >= 255) { | ||||
|             throw "Preferences: key length to big"; | ||||
|             throw "Preferences: key length to big" | ||||
|         } | ||||
|         const cached = this.preferenceSources.get(key) | ||||
|         if (cached !== undefined) { | ||||
|             return cached; | ||||
|             return cached | ||||
|         } | ||||
|         if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) { | ||||
|             this.UpdatePreferences(); | ||||
|             this.UpdatePreferences() | ||||
|         } | ||||
| 
 | ||||
|         const pref = new UIEventSource<string>(this.preferences.data[key] ?? defaultValue, "osm-preference:" + key); | ||||
|         const pref = new UIEventSource<string>( | ||||
|             this.preferences.data[key] ?? defaultValue, | ||||
|             "osm-preference:" + key | ||||
|         ) | ||||
|         pref.addCallback((v) => { | ||||
|             this.UploadPreference(key, v); | ||||
|         }); | ||||
| 
 | ||||
|             this.UploadPreference(key, v) | ||||
|         }) | ||||
| 
 | ||||
|         this.preferenceSources.set(key, pref) | ||||
|         return pref; | ||||
|         return pref | ||||
|     } | ||||
| 
 | ||||
|     public ClearPreferences() { | ||||
|         let isRunning = false; | ||||
|         const self = this; | ||||
|         this.preferences.addCallback(prefs => { | ||||
|         let isRunning = false | ||||
|         const self = this | ||||
|         this.preferences.addCallback((prefs) => { | ||||
|             console.log("Cleaning preferences...") | ||||
|             if (Object.keys(prefs).length == 0) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             if (isRunning) { | ||||
|                 return | ||||
|  | @ -149,41 +162,42 @@ export class OsmPreferences { | |||
|             isRunning = true | ||||
|             const prefixes = ["mapcomplete-"] | ||||
|             for (const key in prefs) { | ||||
|                 const matches = prefixes.some(prefix => key.startsWith(prefix)) | ||||
|                 const matches = prefixes.some((prefix) => key.startsWith(prefix)) | ||||
|                 if (matches) { | ||||
|                     console.log("Clearing ", key) | ||||
|                     self.GetPreference(key, "", "").setData("") | ||||
| 
 | ||||
|                 } | ||||
|             } | ||||
|             isRunning = false; | ||||
|             return; | ||||
|             isRunning = false | ||||
|             return | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private UpdatePreferences() { | ||||
|         const self = this; | ||||
|         this.auth.xhr({ | ||||
|             method: 'GET', | ||||
|             path: '/api/0.6/user/preferences' | ||||
|         }, function (error, value: XMLDocument) { | ||||
|         const self = this | ||||
|         this.auth.xhr( | ||||
|             { | ||||
|                 method: "GET", | ||||
|                 path: "/api/0.6/user/preferences", | ||||
|             }, | ||||
|             function (error, value: XMLDocument) { | ||||
|                 if (error) { | ||||
|                 console.log("Could not load preferences", error); | ||||
|                 return; | ||||
|                     console.log("Could not load preferences", error) | ||||
|                     return | ||||
|                 } | ||||
|             const prefs = value.getElementsByTagName("preference"); | ||||
|                 const prefs = value.getElementsByTagName("preference") | ||||
|                 for (let i = 0; i < prefs.length; i++) { | ||||
|                 const pref = prefs[i]; | ||||
|                 const k = pref.getAttribute("k"); | ||||
|                 const v = pref.getAttribute("v"); | ||||
|                 self.preferences.data[k] = v; | ||||
|                     const pref = prefs[i] | ||||
|                     const k = pref.getAttribute("k") | ||||
|                     const v = pref.getAttribute("v") | ||||
|                     self.preferences.data[k] = v | ||||
|                 } | ||||
| 
 | ||||
|                 // We merge all the preferences: new keys are uploaded
 | ||||
|                 // For differing values, the server overrides local changes
 | ||||
|                 self.preferenceSources.forEach((preference, key) => { | ||||
|                     const osmValue = self.preferences.data[key] | ||||
|                 if(osmValue === undefined && preference.data !== undefined){ | ||||
|                     if (osmValue === undefined && preference.data !== undefined) { | ||||
|                         // OSM doesn't know this value yet
 | ||||
|                         self.UploadPreference(key, preference.data) | ||||
|                     } else { | ||||
|  | @ -192,51 +206,54 @@ export class OsmPreferences { | |||
|                     } | ||||
|                 }) | ||||
| 
 | ||||
|             self.preferences.ping(); | ||||
|         }); | ||||
|                 self.preferences.ping() | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private UploadPreference(k: string, v: string) { | ||||
|         if (!this.userDetails.data.loggedIn) { | ||||
|             console.debug(`Not saving preference ${k}: user not logged in`); | ||||
|             return; | ||||
|             console.debug(`Not saving preference ${k}: user not logged in`) | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (this.preferences.data[k] === v) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15)); | ||||
|         console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15)) | ||||
| 
 | ||||
|         if (v === undefined || v === "") { | ||||
|             this.auth.xhr({ | ||||
|                 method: 'DELETE', | ||||
|                 path: '/api/0.6/user/preferences/' + encodeURIComponent(k), | ||||
|                 options: {header: {'Content-Type': 'text/plain'}}, | ||||
|             }, function (error) { | ||||
|             this.auth.xhr( | ||||
|                 { | ||||
|                     method: "DELETE", | ||||
|                     path: "/api/0.6/user/preferences/" + encodeURIComponent(k), | ||||
|                     options: { header: { "Content-Type": "text/plain" } }, | ||||
|                 }, | ||||
|                 function (error) { | ||||
|                     if (error) { | ||||
|                     console.warn("Could not remove preference", error); | ||||
|                     return; | ||||
|                         console.warn("Could not remove preference", error) | ||||
|                         return | ||||
|                     } | ||||
|                 console.debug("Preference ", k, "removed!"); | ||||
| 
 | ||||
|             }); | ||||
|             return; | ||||
|                     console.debug("Preference ", k, "removed!") | ||||
|                 } | ||||
|             ) | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         this.auth.xhr({ | ||||
|             method: 'PUT', | ||||
|             path: '/api/0.6/user/preferences/' + encodeURIComponent(k), | ||||
|             options: {header: {'Content-Type': 'text/plain'}}, | ||||
|             content: v | ||||
|         }, function (error) { | ||||
|         this.auth.xhr( | ||||
|             { | ||||
|                 method: "PUT", | ||||
|                 path: "/api/0.6/user/preferences/" + encodeURIComponent(k), | ||||
|                 options: { header: { "Content-Type": "text/plain" } }, | ||||
|                 content: v, | ||||
|             }, | ||||
|             function (error) { | ||||
|                 if (error) { | ||||
|                 console.warn(`Could not set preference "${k}"'`, error); | ||||
|                 return; | ||||
|                     console.warn(`Could not set preference "${k}"'`, error) | ||||
|                     return | ||||
|                 } | ||||
|             console.debug(`Preference ${k} written!`); | ||||
|         }); | ||||
|                 console.debug(`Preference ${k} written!`) | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,56 +1,67 @@ | |||
| import {TagsFilter} from "../Tags/TagsFilter"; | ||||
| import RelationsTracker from "./RelationsTracker"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {ImmutableStore, Store} from "../UIEventSource"; | ||||
| import {BBox} from "../BBox"; | ||||
| import * as osmtogeojson from "osmtogeojson"; | ||||
| import {FeatureCollection} from "@turf/turf"; | ||||
| import { TagsFilter } from "../Tags/TagsFilter" | ||||
| import RelationsTracker from "./RelationsTracker" | ||||
| import { Utils } from "../../Utils" | ||||
| import { ImmutableStore, Store } from "../UIEventSource" | ||||
| import { BBox } from "../BBox" | ||||
| import * as osmtogeojson from "osmtogeojson" | ||||
| import { FeatureCollection } from "@turf/turf" | ||||
| 
 | ||||
| /** | ||||
|  * Interfaces overpass to get all the latest data | ||||
|  */ | ||||
| export class Overpass { | ||||
|     private _filter: TagsFilter | ||||
|     private readonly _interpreterUrl: string; | ||||
|     private readonly _timeout: Store<number>; | ||||
|     private readonly _extraScripts: string[]; | ||||
|     private _includeMeta: boolean; | ||||
|     private _relationTracker: RelationsTracker; | ||||
|     private readonly _interpreterUrl: string | ||||
|     private readonly _timeout: Store<number> | ||||
|     private readonly _extraScripts: string[] | ||||
|     private _includeMeta: boolean | ||||
|     private _relationTracker: RelationsTracker | ||||
| 
 | ||||
|     constructor(filter: TagsFilter, | ||||
|     constructor( | ||||
|         filter: TagsFilter, | ||||
|         extraScripts: string[], | ||||
|         interpreterUrl: string, | ||||
|         timeout?: Store<number>, | ||||
|         relationTracker?: RelationsTracker, | ||||
|                 includeMeta = true) { | ||||
|         this._timeout = timeout ?? new ImmutableStore<number>(90); | ||||
|         this._interpreterUrl = interpreterUrl; | ||||
|         includeMeta = true | ||||
|     ) { | ||||
|         this._timeout = timeout ?? new ImmutableStore<number>(90) | ||||
|         this._interpreterUrl = interpreterUrl | ||||
|         const optimized = filter.optimize() | ||||
|         if(optimized === true || optimized === false){ | ||||
|         if (optimized === true || optimized === false) { | ||||
|             throw "Invalid filter: optimizes to true of false" | ||||
|         } | ||||
|         this._filter = optimized | ||||
|         this._extraScripts = extraScripts; | ||||
|         this._includeMeta = includeMeta; | ||||
|         this._extraScripts = extraScripts | ||||
|         this._includeMeta = includeMeta | ||||
|         this._relationTracker = relationTracker | ||||
|     } | ||||
| 
 | ||||
|     public async queryGeoJson(bounds: BBox): Promise<[FeatureCollection, Date]> { | ||||
|         const bbox = "[bbox:" + bounds.getSouth() + "," + bounds.getWest() + "," + bounds.getNorth() + "," + bounds.getEast() + "]"; | ||||
|         const bbox = | ||||
|             "[bbox:" + | ||||
|             bounds.getSouth() + | ||||
|             "," + | ||||
|             bounds.getWest() + | ||||
|             "," + | ||||
|             bounds.getNorth() + | ||||
|             "," + | ||||
|             bounds.getEast() + | ||||
|             "]" | ||||
|         const query = this.buildScript(bbox) | ||||
|         return this.ExecuteQuery(query); | ||||
|         return this.ExecuteQuery(query) | ||||
|     } | ||||
| 
 | ||||
|     public buildUrl(query: string){ | ||||
|     public buildUrl(query: string) { | ||||
|         return `${this._interpreterUrl}?data=${encodeURIComponent(query)}` | ||||
|     } | ||||
| 
 | ||||
|     public async ExecuteQuery(query: string):Promise<[FeatureCollection, Date]>  { | ||||
|         const self = this; | ||||
|     public async ExecuteQuery(query: string): Promise<[FeatureCollection, Date]> { | ||||
|         const self = this | ||||
|         const json = await Utils.downloadJson(this.buildUrl(query)) | ||||
| 
 | ||||
|         if (json.elements.length === 0 && json.remark !== undefined) { | ||||
|             console.warn("Timeout or other runtime error while querying overpass", json.remark); | ||||
|             console.warn("Timeout or other runtime error while querying overpass", json.remark) | ||||
|             throw `Runtime error (timeout or similar)${json.remark}` | ||||
|         } | ||||
|         if (json.elements.length === 0) { | ||||
|  | @ -58,9 +69,9 @@ export class Overpass { | |||
|         } | ||||
| 
 | ||||
|         self._relationTracker?.RegisterRelations(json) | ||||
|         const geojson = osmtogeojson.default(json); | ||||
|         const osmTime = new Date(json.osm3s.timestamp_osm_base); | ||||
|         return [<any> geojson, osmTime]; | ||||
|         const geojson = osmtogeojson.default(json) | ||||
|         const osmTime = new Date(json.osm3s.timestamp_osm_base) | ||||
|         return [<any>geojson, osmTime] | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -75,50 +86,54 @@ export class Overpass { | |||
|         const filters = this._filter.asOverpass() | ||||
|         let filter = "" | ||||
|         for (const filterOr of filters) { | ||||
|             if(pretty){ | ||||
|             if (pretty) { | ||||
|                 filter += "    " | ||||
|             } | ||||
|             filter += 'nwr' + filterOr + postCall + ';' | ||||
|             if(pretty){ | ||||
|                 filter+="\n" | ||||
|             filter += "nwr" + filterOr + postCall + ";" | ||||
|             if (pretty) { | ||||
|                 filter += "\n" | ||||
|             } | ||||
|         } | ||||
|         for (const extraScript of this._extraScripts) { | ||||
|             filter += '(' + extraScript + ');'; | ||||
|             filter += "(" + extraScript + ");" | ||||
|         } | ||||
|         return`[out:json][timeout:${this._timeout.data}]${bbox};(${filter});out body;${this._includeMeta ? 'out meta;' : ''}>;out skel qt;` | ||||
|         return `[out:json][timeout:${this._timeout.data}]${bbox};(${filter});out body;${ | ||||
|             this._includeMeta ? "out meta;" : "" | ||||
|         }>;out skel qt;` | ||||
|     } | ||||
|     /** | ||||
|      * Constructs the actual script to execute on Overpass with geocoding | ||||
|      * 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink' | ||||
|      * | ||||
|      */ | ||||
|     public buildScriptInArea(area: {osm_type: "way" | "relation", osm_id: number}, pretty = false): string { | ||||
|     public buildScriptInArea( | ||||
|         area: { osm_type: "way" | "relation"; osm_id: number }, | ||||
|         pretty = false | ||||
|     ): string { | ||||
|         const filters = this._filter.asOverpass() | ||||
|         let filter = "" | ||||
|         for (const filterOr of filters) { | ||||
|             if(pretty){ | ||||
|             if (pretty) { | ||||
|                 filter += "    " | ||||
|             } | ||||
|             filter += 'nwr' + filterOr + '(area.searchArea);' | ||||
|             if(pretty){ | ||||
|                 filter+="\n" | ||||
|             filter += "nwr" + filterOr + "(area.searchArea);" | ||||
|             if (pretty) { | ||||
|                 filter += "\n" | ||||
|             } | ||||
|         } | ||||
|         for (const extraScript of this._extraScripts) { | ||||
|             filter += '(' + extraScript + ');'; | ||||
|             filter += "(" + extraScript + ");" | ||||
|         } | ||||
|         let id = area.osm_id; | ||||
|         if(area.osm_type === "relation"){ | ||||
|         let id = area.osm_id | ||||
|         if (area.osm_type === "relation") { | ||||
|             id += 3600000000 | ||||
|         } | ||||
|         return`[out:json][timeout:${this._timeout.data}];
 | ||||
|         return `[out:json][timeout:${this._timeout.data}];
 | ||||
|         area(id:${id})->.searchArea; | ||||
|         (${filter}); | ||||
|         out body;${this._includeMeta ? 'out meta;' : ''}>;out skel qt;` | ||||
|         out body;${this._includeMeta ? "out meta;" : ""}>;out skel qt;` | ||||
|     } | ||||
| 
 | ||||
|      | ||||
|     public buildQuery(bbox: string) { | ||||
|         return this.buildUrl(this.buildScript(bbox)) | ||||
|     } | ||||
|  | @ -126,9 +141,9 @@ export class Overpass { | |||
|     /** | ||||
|      * Little helper method to quickly open overpass-turbo in the browser | ||||
|      */ | ||||
|     public static AsOverpassTurboLink(tags: TagsFilter){ | ||||
|     public static AsOverpassTurboLink(tags: TagsFilter) { | ||||
|         const overpass = new Overpass(tags, [], "", undefined, undefined, false) | ||||
|         const script = overpass.buildScript("","({{bbox}})", true) | ||||
|         const script = overpass.buildScript("", "({{bbox}})", true) | ||||
|         const url = "http://overpass-turbo.eu/?Q=" | ||||
|         return url + encodeURIComponent(script) | ||||
|     } | ||||
|  |  | |||
|  | @ -1,24 +1,25 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| 
 | ||||
| export interface Relation { | ||||
|     id: number, | ||||
|     id: number | ||||
|     type: "relation" | ||||
|     members: { | ||||
|         type: ("way" | "node" | "relation"), | ||||
|         ref: number, | ||||
|         type: "way" | "node" | "relation" | ||||
|         ref: number | ||||
|         role: string | ||||
|     }[], | ||||
|     tags: any, | ||||
|     }[] | ||||
|     tags: any | ||||
|     // Alias for tags; tags == properties
 | ||||
|     properties: any | ||||
| } | ||||
| 
 | ||||
| export default class RelationsTracker { | ||||
|     public knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>( | ||||
|         new Map(), | ||||
|         "Relation memberships" | ||||
|     ) | ||||
| 
 | ||||
|     public knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(new Map(), "Relation memberships"); | ||||
| 
 | ||||
|     constructor() { | ||||
|     } | ||||
|     constructor() {} | ||||
| 
 | ||||
|     /** | ||||
|      * Gets an overview of the relations - except for multipolygons. We don't care about those | ||||
|  | @ -26,8 +27,9 @@ export default class RelationsTracker { | |||
|      * @constructor | ||||
|      */ | ||||
|     private static GetRelationElements(overpassJson: any): Relation[] { | ||||
|         const relations = overpassJson.elements | ||||
|             .filter(element => element.type === "relation" && element.tags.type !== "multipolygon") | ||||
|         const relations = overpassJson.elements.filter( | ||||
|             (element) => element.type === "relation" && element.tags.type !== "multipolygon" | ||||
|         ) | ||||
|         for (const relation of relations) { | ||||
|             relation.properties = relation.tags | ||||
|         } | ||||
|  | @ -45,12 +47,12 @@ export default class RelationsTracker { | |||
|      */ | ||||
|     private UpdateMembershipTable(relations: Relation[]): void { | ||||
|         const memberships = this.knownRelations.data | ||||
|         let changed = false; | ||||
|         let changed = false | ||||
|         for (const relation of relations) { | ||||
|             for (const member of relation.members) { | ||||
|                 const role = { | ||||
|                     role: member.role, | ||||
|                     relation: relation | ||||
|                     relation: relation, | ||||
|                 } | ||||
|                 const key = member.type + "/" + member.ref | ||||
|                 if (!memberships.has(key)) { | ||||
|  | @ -58,19 +60,17 @@ export default class RelationsTracker { | |||
|                 } | ||||
|                 const knownRelations = memberships.get(key) | ||||
| 
 | ||||
|                 const alreadyExists = knownRelations.some(knownRole => { | ||||
|                 const alreadyExists = knownRelations.some((knownRole) => { | ||||
|                     return knownRole.role === role.role && knownRole.relation === role.relation | ||||
|                 }) | ||||
|                 if (!alreadyExists) { | ||||
|                     knownRelations.push(role) | ||||
|                     changed = true; | ||||
|                     changed = true | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         if (changed) { | ||||
|             this.knownRelations.ping() | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,13 +1,12 @@ | |||
| export default class AspectedRouting { | ||||
| 
 | ||||
|     public readonly name: string | ||||
|     public readonly description: string | ||||
|     public readonly units: string | ||||
|     public readonly program: any | ||||
| 
 | ||||
|     public constructor(program) { | ||||
|         this.name = program.name; | ||||
|         this.description = program.description; | ||||
|         this.name = program.name | ||||
|         this.description = program.description | ||||
|         this.units = program.unit | ||||
|         this.program = JSON.parse(JSON.stringify(program)) | ||||
|         delete this.program.name | ||||
|  | @ -20,40 +19,41 @@ export default class AspectedRouting { | |||
|      */ | ||||
|     public static interpret(program: any, properties: any) { | ||||
|         if (typeof program !== "object") { | ||||
|             return program; | ||||
|             return program | ||||
|         } | ||||
| 
 | ||||
|         let functionName /*: string*/ = undefined; | ||||
|         let functionName /*: string*/ = undefined | ||||
|         let functionArguments /*: any */ = undefined | ||||
|         let otherValues = {} | ||||
|         // @ts-ignore
 | ||||
|         Object.entries(program).forEach(tag => { | ||||
|                 const [key, value] = tag; | ||||
|         Object.entries(program).forEach((tag) => { | ||||
|             const [key, value] = tag | ||||
|             if (key.startsWith("$")) { | ||||
|                 functionName = key | ||||
|                 functionArguments = value | ||||
|             } else { | ||||
|                 otherValues[key] = value | ||||
|             } | ||||
|             } | ||||
|         ) | ||||
|         }) | ||||
| 
 | ||||
|         if (functionName === undefined) { | ||||
|             return AspectedRouting.interpretAsDictionary(program, properties) | ||||
|         } | ||||
| 
 | ||||
|         if (functionName === '$multiply') { | ||||
|             return AspectedRouting.multiplyScore(properties, functionArguments); | ||||
|         } else if (functionName === '$firstMatchOf') { | ||||
|             return AspectedRouting.getFirstMatchScore(properties, functionArguments); | ||||
|         } else if (functionName === '$min') { | ||||
|             return AspectedRouting.getMinValue(properties, functionArguments); | ||||
|         } else if (functionName === '$max') { | ||||
|             return AspectedRouting.getMaxValue(properties, functionArguments); | ||||
|         } else if (functionName === '$default') { | ||||
|         if (functionName === "$multiply") { | ||||
|             return AspectedRouting.multiplyScore(properties, functionArguments) | ||||
|         } else if (functionName === "$firstMatchOf") { | ||||
|             return AspectedRouting.getFirstMatchScore(properties, functionArguments) | ||||
|         } else if (functionName === "$min") { | ||||
|             return AspectedRouting.getMinValue(properties, functionArguments) | ||||
|         } else if (functionName === "$max") { | ||||
|             return AspectedRouting.getMaxValue(properties, functionArguments) | ||||
|         } else if (functionName === "$default") { | ||||
|             return AspectedRouting.defaultV(functionArguments, otherValues, properties) | ||||
|         } else { | ||||
|             console.error(`Error: Program ${functionName} is not implemented yet. ${JSON.stringify(program)}`); | ||||
|             console.error( | ||||
|                 `Error: Program ${functionName} is not implemented yet. ${JSON.stringify(program)}` | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -86,8 +86,8 @@ export default class AspectedRouting { | |||
|      */ | ||||
|     private static interpretAsDictionary(program, tags) { | ||||
|         // @ts-ignore
 | ||||
|         return Object.entries(tags).map(tag => { | ||||
|             const [key, value] = tag; | ||||
|         return Object.entries(tags).map((tag) => { | ||||
|             const [key, value] = tag | ||||
|             const propertyValue = program[key] | ||||
|             if (propertyValue === undefined) { | ||||
|                 return undefined | ||||
|  | @ -97,7 +97,7 @@ export default class AspectedRouting { | |||
|             } | ||||
|             // @ts-ignore
 | ||||
|             return propertyValue[value] | ||||
|         }); | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private static defaultV(subProgram, otherArgs, tags) { | ||||
|  | @ -105,7 +105,7 @@ export default class AspectedRouting { | |||
|         const normalProgram = Object.entries(otherArgs)[0][1] | ||||
|         const value = AspectedRouting.interpret(normalProgram, tags) | ||||
|         if (value !== undefined) { | ||||
|             return value; | ||||
|             return value | ||||
|         } | ||||
|         return AspectedRouting.interpret(subProgram, tags) | ||||
|     } | ||||
|  | @ -121,13 +121,15 @@ export default class AspectedRouting { | |||
| 
 | ||||
|         let subResults: any[] | ||||
|         if (subprograms.length !== undefined) { | ||||
|             subResults = AspectedRouting.concatMap(subprograms, subprogram => AspectedRouting.interpret(subprogram, tags)) | ||||
|             subResults = AspectedRouting.concatMap(subprograms, (subprogram) => | ||||
|                 AspectedRouting.interpret(subprogram, tags) | ||||
|             ) | ||||
|         } else { | ||||
|             subResults = AspectedRouting.interpret(subprograms, tags) | ||||
|         } | ||||
| 
 | ||||
|         subResults.filter(r => r !== undefined).forEach(r => number *= parseFloat(r)) | ||||
|         return number.toFixed(2); | ||||
|         subResults.filter((r) => r !== undefined).forEach((r) => (number *= parseFloat(r))) | ||||
|         return number.toFixed(2) | ||||
|     } | ||||
| 
 | ||||
|     private static getFirstMatchScore(tags, order: any) { | ||||
|  | @ -136,12 +138,12 @@ export default class AspectedRouting { | |||
|         for (let key of order) { | ||||
|             // @ts-ignore
 | ||||
|             for (let entry of Object.entries(JSON.parse(tags))) { | ||||
|                 const [tagKey, value] = entry; | ||||
|                 const [tagKey, value] = entry | ||||
|                 if (key === tagKey) { | ||||
|                     // We have a match... let's evaluate the subprogram
 | ||||
|                     const evaluated = AspectedRouting.interpret(value, tags) | ||||
|                     if (evaluated !== undefined) { | ||||
|                         return evaluated; | ||||
|                         return evaluated | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | @ -152,26 +154,30 @@ export default class AspectedRouting { | |||
|     } | ||||
| 
 | ||||
|     private static getMinValue(tags, subprogram) { | ||||
|         const minArr = subprogram.map(part => { | ||||
|             if (typeof (part) === 'object') { | ||||
|         const minArr = subprogram | ||||
|             .map((part) => { | ||||
|                 if (typeof part === "object") { | ||||
|                     const calculatedValue = this.interpret(part, tags) | ||||
|                     return parseFloat(calculatedValue) | ||||
|                 } else { | ||||
|                 return parseFloat(part); | ||||
|                     return parseFloat(part) | ||||
|                 } | ||||
|         }).filter(v => !isNaN(v)); | ||||
|         return Math.min(...minArr); | ||||
|             }) | ||||
|             .filter((v) => !isNaN(v)) | ||||
|         return Math.min(...minArr) | ||||
|     } | ||||
| 
 | ||||
|     private static getMaxValue(tags, subprogram) { | ||||
|         const maxArr = subprogram.map(part => { | ||||
|             if (typeof (part) === 'object') { | ||||
|         const maxArr = subprogram | ||||
|             .map((part) => { | ||||
|                 if (typeof part === "object") { | ||||
|                     return parseFloat(AspectedRouting.interpret(part, tags)) | ||||
|                 } else { | ||||
|                 return parseFloat(part); | ||||
|                     return parseFloat(part) | ||||
|                 } | ||||
|         }).filter(v => !isNaN(v)); | ||||
|         return Math.max(...maxArr); | ||||
|             }) | ||||
|             .filter((v) => !isNaN(v)) | ||||
|         return Math.max(...maxArr) | ||||
|     } | ||||
| 
 | ||||
|     private static concatMap(list, f): any[] { | ||||
|  | @ -185,11 +191,10 @@ export default class AspectedRouting { | |||
|                 result.push(elem) | ||||
|             } | ||||
|         } | ||||
|         return result; | ||||
|         return result | ||||
|     } | ||||
| 
 | ||||
|     public evaluate(properties) { | ||||
|         return AspectedRouting.interpret(this.program, properties) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,107 +1,125 @@ | |||
| import {GeoOperations} from "./GeoOperations"; | ||||
| import {Utils} from "../Utils"; | ||||
| import opening_hours from "opening_hours"; | ||||
| import Combine from "../UI/Base/Combine"; | ||||
| import BaseUIElement from "../UI/BaseUIElement"; | ||||
| import Title from "../UI/Base/Title"; | ||||
| import {FixedUiElement} from "../UI/Base/FixedUiElement"; | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| import {CountryCoder} from "latlon2country" | ||||
| import Constants from "../Models/Constants"; | ||||
| import {TagUtils} from "./Tags/TagUtils"; | ||||
| 
 | ||||
| import { GeoOperations } from "./GeoOperations" | ||||
| import { Utils } from "../Utils" | ||||
| import opening_hours from "opening_hours" | ||||
| import Combine from "../UI/Base/Combine" | ||||
| import BaseUIElement from "../UI/BaseUIElement" | ||||
| import Title from "../UI/Base/Title" | ||||
| import { FixedUiElement } from "../UI/Base/FixedUiElement" | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||
| import { CountryCoder } from "latlon2country" | ||||
| import Constants from "../Models/Constants" | ||||
| import { TagUtils } from "./Tags/TagUtils" | ||||
| 
 | ||||
| export class SimpleMetaTagger { | ||||
|     public readonly keys: string[]; | ||||
|     public readonly doc: string; | ||||
|     public readonly isLazy: boolean; | ||||
|     public readonly keys: string[] | ||||
|     public readonly doc: string | ||||
|     public readonly isLazy: boolean | ||||
|     public readonly includesDates: boolean | ||||
|     public readonly applyMetaTagsOnFeature: (feature: any, freshness: Date, layer: LayerConfig, state) => boolean; | ||||
|     public readonly applyMetaTagsOnFeature: ( | ||||
|         feature: any, | ||||
|         freshness: Date, | ||||
|         layer: LayerConfig, | ||||
|         state | ||||
|     ) => boolean | ||||
| 
 | ||||
|     /*** | ||||
|      * A function that adds some extra data to a feature | ||||
|      * @param docs: what does this extra data do? | ||||
|      * @param f: apply the changes. Returns true if something changed | ||||
|      */ | ||||
|     constructor(docs: { keys: string[], doc: string, includesDates?: boolean, isLazy?: boolean, cleanupRetagger?: boolean }, | ||||
|                 f: ((feature: any, freshness: Date, layer: LayerConfig, state) => boolean)) { | ||||
|         this.keys = docs.keys; | ||||
|         this.doc = docs.doc; | ||||
|     constructor( | ||||
|         docs: { | ||||
|             keys: string[] | ||||
|             doc: string | ||||
|             includesDates?: boolean | ||||
|             isLazy?: boolean | ||||
|             cleanupRetagger?: boolean | ||||
|         }, | ||||
|         f: (feature: any, freshness: Date, layer: LayerConfig, state) => boolean | ||||
|     ) { | ||||
|         this.keys = docs.keys | ||||
|         this.doc = docs.doc | ||||
|         this.isLazy = docs.isLazy | ||||
|         this.applyMetaTagsOnFeature = f; | ||||
|         this.includesDates = docs.includesDates ?? false; | ||||
|         this.applyMetaTagsOnFeature = f | ||||
|         this.includesDates = docs.includesDates ?? false | ||||
|         if (!docs.cleanupRetagger) { | ||||
|             for (const key of docs.keys) { | ||||
|                 if (!key.startsWith('_') && key.toLowerCase().indexOf("theme") < 0) { | ||||
|                 if (!key.startsWith("_") && key.toLowerCase().indexOf("theme") < 0) { | ||||
|                     throw `Incorrect key for a calculated meta value '${key}': it should start with underscore (_)` | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class CountryTagger extends SimpleMetaTagger { | ||||
|     private static readonly coder = new CountryCoder(Constants.countryCoderEndpoint, Utils.downloadJson); | ||||
|     public runningTasks: Set<any>; | ||||
|     private static readonly coder = new CountryCoder( | ||||
|         Constants.countryCoderEndpoint, | ||||
|         Utils.downloadJson | ||||
|     ) | ||||
|     public runningTasks: Set<any> | ||||
| 
 | ||||
|     constructor() { | ||||
|         const runningTasks = new Set<any>(); | ||||
|         super | ||||
|         ( | ||||
|         const runningTasks = new Set<any>() | ||||
|         super( | ||||
|             { | ||||
|                 keys: ["_country"], | ||||
|                 doc: "The country code of the property (with latlon2country)", | ||||
|                 includesDates: false | ||||
|                 includesDates: false, | ||||
|             }, | ||||
|             ((feature, _, __, state) => { | ||||
|                 let centerPoint: any = GeoOperations.centerpoint(feature); | ||||
|                 const lat = centerPoint.geometry.coordinates[1]; | ||||
|                 const lon = centerPoint.geometry.coordinates[0]; | ||||
|             (feature, _, __, state) => { | ||||
|                 let centerPoint: any = GeoOperations.centerpoint(feature) | ||||
|                 const lat = centerPoint.geometry.coordinates[1] | ||||
|                 const lon = centerPoint.geometry.coordinates[0] | ||||
|                 runningTasks.add(feature) | ||||
|                 CountryTagger.coder.GetCountryCodeAsync(lon, lat).then( | ||||
|                     countries => { | ||||
|                 CountryTagger.coder | ||||
|                     .GetCountryCodeAsync(lon, lat) | ||||
|                     .then((countries) => { | ||||
|                         runningTasks.delete(feature) | ||||
|                         try { | ||||
|                             const oldCountry = feature.properties["_country"]; | ||||
|                             feature.properties["_country"] = countries[0].trim().toLowerCase(); | ||||
|                             const oldCountry = feature.properties["_country"] | ||||
|                             feature.properties["_country"] = countries[0].trim().toLowerCase() | ||||
|                             if (oldCountry !== feature.properties["_country"]) { | ||||
|                                 const tagsSource = state?.allElements?.getEventSourceById(feature.properties.id); | ||||
|                                 tagsSource?.ping(); | ||||
|                                 const tagsSource = state?.allElements?.getEventSourceById( | ||||
|                                     feature.properties.id | ||||
|                                 ) | ||||
|                                 tagsSource?.ping() | ||||
|                             } | ||||
|                         } catch (e) { | ||||
|                             console.warn(e) | ||||
|                         } | ||||
|                     } | ||||
|                 ).catch(_ => { | ||||
|                     }) | ||||
|                     .catch((_) => { | ||||
|                         runningTasks.delete(feature) | ||||
|                     }) | ||||
|                 return false; | ||||
|             }) | ||||
|                 return false | ||||
|             } | ||||
|         ) | ||||
|         this.runningTasks = runningTasks; | ||||
|         this.runningTasks = runningTasks | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default class SimpleMetaTaggers { | ||||
| 
 | ||||
|     public static readonly objectMetaInfo = new SimpleMetaTagger( | ||||
|         { | ||||
|             keys: ["_last_edit:contributor", | ||||
|             keys: [ | ||||
|                 "_last_edit:contributor", | ||||
|                 "_last_edit:contributor:uid", | ||||
|                 "_last_edit:changeset", | ||||
|                 "_last_edit:timestamp", | ||||
|                 "_version_number", | ||||
|                 "_backend"], | ||||
|             doc: "Information about the last edit of this object." | ||||
|                 "_backend", | ||||
|             ], | ||||
|             doc: "Information about the last edit of this object.", | ||||
|         }, | ||||
|         (feature) => {/*Note: also called by 'UpdateTagsFromOsmAPI'*/ | ||||
|         (feature) => { | ||||
|             /*Note: also called by 'UpdateTagsFromOsmAPI'*/ | ||||
| 
 | ||||
|             const tgs = feature.properties; | ||||
|             const tgs = feature.properties | ||||
| 
 | ||||
|             function move(src: string, target: string) { | ||||
|                 if (tgs[src] === undefined) { | ||||
|                     return; | ||||
|                     return | ||||
|                 } | ||||
|                 tgs[target] = tgs[src] | ||||
|                 delete tgs[src] | ||||
|  | @ -112,7 +130,7 @@ export default class SimpleMetaTaggers { | |||
|             move("changeset", "_last_edit:changeset") | ||||
|             move("timestamp", "_last_edit:timestamp") | ||||
|             move("version", "_version_number") | ||||
|             return true; | ||||
|             return true | ||||
|         } | ||||
|     ) | ||||
|     public static country = new CountryTagger() | ||||
|  | @ -122,32 +140,45 @@ export default class SimpleMetaTaggers { | |||
|             doc: "Adds the geometry type as property. This is identical to the GoeJson geometry type and is one of `Point`,`LineString`, `Polygon` and exceptionally `MultiPolygon` or `MultiLineString`", | ||||
|         }, | ||||
|         (feature, _) => { | ||||
|             const changed = feature.properties["_geometry:type"] === feature.geometry.type; | ||||
|             feature.properties["_geometry:type"] = feature.geometry.type; | ||||
|             const changed = feature.properties["_geometry:type"] === feature.geometry.type | ||||
|             feature.properties["_geometry:type"] = feature.geometry.type | ||||
|             return changed | ||||
|         } | ||||
|     ) | ||||
|     private static readonly cardinalDirections = { | ||||
|         N: 0, NNE: 22.5, NE: 45, ENE: 67.5, | ||||
|         E: 90, ESE: 112.5, SE: 135, SSE: 157.5, | ||||
|         S: 180, SSW: 202.5, SW: 225, WSW: 247.5, | ||||
|         W: 270, WNW: 292.5, NW: 315, NNW: 337.5 | ||||
|         N: 0, | ||||
|         NNE: 22.5, | ||||
|         NE: 45, | ||||
|         ENE: 67.5, | ||||
|         E: 90, | ||||
|         ESE: 112.5, | ||||
|         SE: 135, | ||||
|         SSE: 157.5, | ||||
|         S: 180, | ||||
|         SSW: 202.5, | ||||
|         SW: 225, | ||||
|         WSW: 247.5, | ||||
|         W: 270, | ||||
|         WNW: 292.5, | ||||
|         NW: 315, | ||||
|         NNW: 337.5, | ||||
|     } | ||||
|     private static latlon = new SimpleMetaTagger({ | ||||
|     private static latlon = new SimpleMetaTagger( | ||||
|         { | ||||
|             keys: ["_lat", "_lon"], | ||||
|             doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)" | ||||
|             doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)", | ||||
|         }, | ||||
|         (feature => { | ||||
|             const centerPoint = GeoOperations.centerpoint(feature); | ||||
|             const lat = centerPoint.geometry.coordinates[1]; | ||||
|             const lon = centerPoint.geometry.coordinates[0]; | ||||
|             feature.properties["_lat"] = "" + lat; | ||||
|             feature.properties["_lon"] = "" + lon; | ||||
|             feature._lon = lon; // This is dirty, I know
 | ||||
|             feature._lat = lat; | ||||
|             return true; | ||||
|         }) | ||||
|     ); | ||||
|         (feature) => { | ||||
|             const centerPoint = GeoOperations.centerpoint(feature) | ||||
|             const lat = centerPoint.geometry.coordinates[1] | ||||
|             const lon = centerPoint.geometry.coordinates[0] | ||||
|             feature.properties["_lat"] = "" + lat | ||||
|             feature.properties["_lon"] = "" + lon | ||||
|             feature._lon = lon // This is dirty, I know
 | ||||
|             feature._lat = lat | ||||
|             return true | ||||
|         } | ||||
|     ) | ||||
|     private static layerInfo = new SimpleMetaTagger( | ||||
|         { | ||||
|             doc: "The layer-id to which this feature belongs. Note that this might be return any applicable if `passAllFeatures` is defined.", | ||||
|  | @ -156,98 +187,101 @@ export default class SimpleMetaTaggers { | |||
|         }, | ||||
|         (feature, freshness, layer) => { | ||||
|             if (feature.properties._layer === layer.id) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
|             feature.properties._layer = layer.id | ||||
|             return true; | ||||
|             return true | ||||
|         } | ||||
|     ) | ||||
|     private static noBothButLeftRight = new SimpleMetaTagger( | ||||
|         { | ||||
|             keys: ["sidewalk:left", "sidewalk:right", "generic_key:left:property", "generic_key:right:property"], | ||||
|             keys: [ | ||||
|                 "sidewalk:left", | ||||
|                 "sidewalk:right", | ||||
|                 "generic_key:left:property", | ||||
|                 "generic_key:right:property", | ||||
|             ], | ||||
|             doc: "Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' and 'generic_key:right:property' (and similar for sidewalk tagging). Note that this rewritten tags _will be reuploaded on a change_. To prevent to much unrelated retagging, this is only enabled if the layer has at least some lineRenderings with offset defined", | ||||
|             includesDates: false, | ||||
|             cleanupRetagger: true | ||||
|             cleanupRetagger: true, | ||||
|         }, | ||||
|         ((feature, state, layer) => { | ||||
| 
 | ||||
|             if (!layer.lineRendering.some(lr => lr.leftRightSensitive)) { | ||||
|                 return; | ||||
|         (feature, state, layer) => { | ||||
|             if (!layer.lineRendering.some((lr) => lr.leftRightSensitive)) { | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             return SimpleMetaTaggers.removeBothTagging(feature.properties) | ||||
|         }) | ||||
|         } | ||||
|     ) | ||||
|     private static surfaceArea = new SimpleMetaTagger( | ||||
|         { | ||||
|             keys: ["_surface", "_surface:ha"], | ||||
|             doc: "The surface area of the feature, in square meters and in hectare. Not set on points and ways", | ||||
|             isLazy: true | ||||
|             isLazy: true, | ||||
|         }, | ||||
|         (feature => { | ||||
| 
 | ||||
|         (feature) => { | ||||
|             Object.defineProperty(feature.properties, "_surface", { | ||||
|                 enumerable: false, | ||||
|                 configurable: true, | ||||
|                 get: () => { | ||||
|                     const sqMeters = "" + GeoOperations.surfaceAreaInSqMeters(feature); | ||||
|                     const sqMeters = "" + GeoOperations.surfaceAreaInSqMeters(feature) | ||||
|                     delete feature.properties["_surface"] | ||||
|                     feature.properties["_surface"] = sqMeters; | ||||
|                     feature.properties["_surface"] = sqMeters | ||||
|                     return sqMeters | ||||
|                 } | ||||
|                 }, | ||||
|             }) | ||||
| 
 | ||||
|             Object.defineProperty(feature.properties, "_surface:ha", { | ||||
|                 enumerable: false, | ||||
|                 configurable: true, | ||||
|                 get: () => { | ||||
|                     const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature); | ||||
|                     const sqMetersHa = "" + Math.floor(sqMeters / 1000) / 10; | ||||
|                     const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature) | ||||
|                     const sqMetersHa = "" + Math.floor(sqMeters / 1000) / 10 | ||||
|                     delete feature.properties["_surface:ha"] | ||||
|                     feature.properties["_surface:ha"] = sqMetersHa; | ||||
|                     feature.properties["_surface:ha"] = sqMetersHa | ||||
|                     return sqMetersHa | ||||
|                 } | ||||
|                 }, | ||||
|             }) | ||||
| 
 | ||||
|             return true; | ||||
|         }) | ||||
|     ); | ||||
|             return true | ||||
|         } | ||||
|     ) | ||||
|     private static levels = new SimpleMetaTagger( | ||||
|         { | ||||
|             doc: "Extract the 'level'-tag into a normalized, ';'-separated value", | ||||
|             keys: ["_level"] | ||||
|             keys: ["_level"], | ||||
|         }, | ||||
|         ((feature) => { | ||||
|         (feature) => { | ||||
|             if (feature.properties["level"] === undefined) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
| 
 | ||||
|             const l = feature.properties["level"] | ||||
|             const newValue = TagUtils.LevelsParser(l).join(";") | ||||
|             if(l === newValue) { | ||||
|                 return false; | ||||
|             if (l === newValue) { | ||||
|                 return false | ||||
|             } | ||||
|             feature.properties["level"] = newValue | ||||
|             return true | ||||
| 
 | ||||
|         }) | ||||
|         } | ||||
|     ) | ||||
| 
 | ||||
|     private static canonicalize = new SimpleMetaTagger( | ||||
|         { | ||||
|             doc: "If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`; `1` will be rewritten to `1m` as well)", | ||||
|             keys: ["Theme-defined keys"], | ||||
| 
 | ||||
|         }, | ||||
|         ((feature, _, __, state) => { | ||||
|             const units = Utils.NoNull([].concat(...state?.layoutToUse?.layers?.map(layer => layer.units) ?? [])); | ||||
|         (feature, _, __, state) => { | ||||
|             const units = Utils.NoNull( | ||||
|                 [].concat(...(state?.layoutToUse?.layers?.map((layer) => layer.units) ?? [])) | ||||
|             ) | ||||
|             if (units.length == 0) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             let rewritten = false; | ||||
|             let rewritten = false | ||||
|             for (const key in feature.properties) { | ||||
|                 if (!feature.properties.hasOwnProperty(key)) { | ||||
|                     continue; | ||||
|                     continue | ||||
|                 } | ||||
|                 for (const unit of units) { | ||||
|                     if (unit === undefined) { | ||||
|  | @ -258,56 +292,59 @@ export default class SimpleMetaTaggers { | |||
|                         continue | ||||
|                     } | ||||
|                     if (!unit.appliesToKeys.has(key)) { | ||||
|                         continue; | ||||
|                         continue | ||||
|                     } | ||||
|                     const value = feature.properties[key] | ||||
|                     const denom = unit.findDenomination(value, () => feature.properties["_country"]) | ||||
|                     if (denom === undefined) { | ||||
|                         // no valid value found
 | ||||
|                         break; | ||||
|                         break | ||||
|                     } | ||||
|                     const [, denomination] = denom; | ||||
|                     const defaultDenom = unit.getDefaultDenomination(() => feature.properties["_country"]) | ||||
|                     let canonical = denomination?.canonicalValue(value, defaultDenom == denomination) ?? undefined; | ||||
|                     const [, denomination] = denom | ||||
|                     const defaultDenom = unit.getDefaultDenomination( | ||||
|                         () => feature.properties["_country"] | ||||
|                     ) | ||||
|                     let canonical = | ||||
|                         denomination?.canonicalValue(value, defaultDenom == denomination) ?? | ||||
|                         undefined | ||||
|                     if (canonical === value) { | ||||
|                         break; | ||||
|                         break | ||||
|                     } | ||||
|                     console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`) | ||||
|                     if (canonical === undefined && !unit.eraseInvalid) { | ||||
|                         break; | ||||
|                         break | ||||
|                     } | ||||
| 
 | ||||
|                     feature.properties[key] = canonical; | ||||
|                     rewritten = true; | ||||
|                     break; | ||||
|                     feature.properties[key] = canonical | ||||
|                     rewritten = true | ||||
|                     break | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|             return rewritten | ||||
|         }) | ||||
|         } | ||||
|     ) | ||||
|     private static lngth = new SimpleMetaTagger( | ||||
|         { | ||||
|             keys: ["_length", "_length:km"], | ||||
|             doc: "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter" | ||||
|             doc: "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter", | ||||
|         }, | ||||
|         (feature => { | ||||
|         (feature) => { | ||||
|             const l = GeoOperations.lengthInMeters(feature) | ||||
|             feature.properties["_length"] = "" + l | ||||
|             const km = Math.floor(l / 1000) | ||||
|             const kmRest = Math.round((l - km * 1000) / 100) | ||||
|             feature.properties["_length:km"] = "" + km + "." + kmRest | ||||
|             return true; | ||||
|         }) | ||||
|             return true | ||||
|         } | ||||
|     ) | ||||
|     private static isOpen = new SimpleMetaTagger( | ||||
|         { | ||||
|             keys: ["_isOpen"], | ||||
|             doc: "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')", | ||||
|             includesDates: true, | ||||
|             isLazy: true | ||||
|             isLazy: true, | ||||
|         }, | ||||
|         ((feature, _, __, state) => { | ||||
|         (feature, _, __, state) => { | ||||
|             if (Utils.runningFromConsole) { | ||||
|                 // We are running from console, thus probably creating a cache
 | ||||
|                 // isOpen is irrelevant
 | ||||
|  | @ -315,7 +352,7 @@ export default class SimpleMetaTaggers { | |||
|             } | ||||
|             if (feature.properties.opening_hours === "24/7") { | ||||
|                 feature.properties._isOpen = "yes" | ||||
|                 return true; | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             // _isOpen is calculated dynamically on every call
 | ||||
|  | @ -325,92 +362,92 @@ export default class SimpleMetaTaggers { | |||
|                 get: () => { | ||||
|                     const tags = feature.properties | ||||
|                     if (tags.opening_hours === undefined) { | ||||
|                         return; | ||||
|                         return | ||||
|                     } | ||||
|                     if (tags._country === undefined) { | ||||
|                         return; | ||||
|                         return | ||||
|                     } | ||||
| 
 | ||||
|                     try { | ||||
|                         const [lon, lat] = GeoOperations.centerpointCoordinates(feature) | ||||
|                         const oh = new opening_hours(tags["opening_hours"], { | ||||
|                         const oh = new opening_hours( | ||||
|                             tags["opening_hours"], | ||||
|                             { | ||||
|                                 lat: lat, | ||||
|                                 lon: lon, | ||||
|                                 address: { | ||||
|                                     country_code: tags._country.toLowerCase(), | ||||
|                                 state: undefined | ||||
|                             } | ||||
|                         }, <any>{tag_key: "opening_hours"}); | ||||
|                                     state: undefined, | ||||
|                                 }, | ||||
|                             }, | ||||
|                             <any>{ tag_key: "opening_hours" } | ||||
|                         ) | ||||
| 
 | ||||
|                         // Recalculate!
 | ||||
|                         return oh.getState() ? "yes" : "no"; | ||||
| 
 | ||||
|                         return oh.getState() ? "yes" : "no" | ||||
|                     } catch (e) { | ||||
|                         console.warn("Error while parsing opening hours of ", tags.id, e); | ||||
|                         console.warn("Error while parsing opening hours of ", tags.id, e) | ||||
|                         delete tags._isOpen | ||||
|                         tags["_isOpen"] = "parse_error"; | ||||
|                         tags["_isOpen"] = "parse_error" | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
| 
 | ||||
|             const tagsSource = state.allElements.getEventSourceById(feature.properties.id); | ||||
| 
 | ||||
| 
 | ||||
|                 }, | ||||
|             }) | ||||
| 
 | ||||
|             const tagsSource = state.allElements.getEventSourceById(feature.properties.id) | ||||
|         } | ||||
|     ) | ||||
|     private static directionSimplified = new SimpleMetaTagger( | ||||
|         { | ||||
|             keys: ["_direction:numerical", "_direction:leftright"], | ||||
|             doc: "_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map" | ||||
|             doc: "_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map", | ||||
|         }, | ||||
|         (feature => { | ||||
|             const tags = feature.properties; | ||||
|             const direction = tags["camera:direction"] ?? tags["direction"]; | ||||
|         (feature) => { | ||||
|             const tags = feature.properties | ||||
|             const direction = tags["camera:direction"] ?? tags["direction"] | ||||
|             if (direction === undefined) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
|             const n = SimpleMetaTaggers.cardinalDirections[direction] ?? Number(direction); | ||||
|             const n = SimpleMetaTaggers.cardinalDirections[direction] ?? Number(direction) | ||||
|             if (isNaN(n)) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
| 
 | ||||
|             // The % operator has range (-360, 360). We apply a trick to get [0, 360).
 | ||||
|             const normalized = ((n % 360) + 360) % 360; | ||||
|             const normalized = ((n % 360) + 360) % 360 | ||||
| 
 | ||||
|             tags["_direction:numerical"] = normalized; | ||||
|             tags["_direction:leftright"] = normalized <= 180 ? "right" : "left"; | ||||
|             return true; | ||||
|         }) | ||||
|             tags["_direction:numerical"] = normalized | ||||
|             tags["_direction:leftright"] = normalized <= 180 ? "right" : "left" | ||||
|             return true | ||||
|         } | ||||
|     ) | ||||
|     private static currentTime = new SimpleMetaTagger( | ||||
|         { | ||||
|             keys: ["_now:date", "_now:datetime", "_loaded:date", "_loaded:_datetime"], | ||||
|             doc: "Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely", | ||||
|             includesDates: true | ||||
|             includesDates: true, | ||||
|         }, | ||||
|         (feature, freshness) => { | ||||
|             const now = new Date(); | ||||
|             const now = new Date() | ||||
| 
 | ||||
|             if (typeof freshness === "string") { | ||||
|                 freshness = new Date(freshness) | ||||
|             } | ||||
| 
 | ||||
|             function date(d: Date) { | ||||
|                 return d.toISOString().slice(0, 10); | ||||
|                 return d.toISOString().slice(0, 10) | ||||
|             } | ||||
| 
 | ||||
|             function datetime(d: Date) { | ||||
|                 return d.toISOString().slice(0, -5).replace("T", " "); | ||||
|                 return d.toISOString().slice(0, -5).replace("T", " ") | ||||
|             } | ||||
| 
 | ||||
|             feature.properties["_now:date"] = date(now); | ||||
|             feature.properties["_now:datetime"] = datetime(now); | ||||
|             feature.properties["_loaded:date"] = date(freshness); | ||||
|             feature.properties["_loaded:datetime"] = datetime(freshness); | ||||
|             return true; | ||||
|             feature.properties["_now:date"] = date(now) | ||||
|             feature.properties["_now:datetime"] = datetime(now) | ||||
|             feature.properties["_loaded:date"] = date(freshness) | ||||
|             feature.properties["_loaded:datetime"] = datetime(freshness) | ||||
|             return true | ||||
|         } | ||||
|     ); | ||||
|     ) | ||||
|     public static metatags: SimpleMetaTagger[] = [ | ||||
|         SimpleMetaTaggers.latlon, | ||||
|         SimpleMetaTaggers.layerInfo, | ||||
|  | @ -424,11 +461,11 @@ export default class SimpleMetaTaggers { | |||
|         SimpleMetaTaggers.objectMetaInfo, | ||||
|         SimpleMetaTaggers.noBothButLeftRight, | ||||
|         SimpleMetaTaggers.geometryType, | ||||
|         SimpleMetaTaggers.levels | ||||
| 
 | ||||
|     ]; | ||||
|     public static readonly lazyTags: string[] = [].concat(...SimpleMetaTaggers.metatags.filter(tagger => tagger.isLazy) | ||||
|         .map(tagger => tagger.keys)); | ||||
|         SimpleMetaTaggers.levels, | ||||
|     ] | ||||
|     public static readonly lazyTags: string[] = [].concat( | ||||
|         ...SimpleMetaTaggers.metatags.filter((tagger) => tagger.isLazy).map((tagger) => tagger.keys) | ||||
|     ) | ||||
| 
 | ||||
|     /** | ||||
|      * Edits the given object to rewrite 'both'-tagging into a 'left-right' tagging scheme. | ||||
|  | @ -451,36 +488,34 @@ export default class SimpleMetaTaggers { | |||
|         } | ||||
| 
 | ||||
|         if (tags["sidewalk"]) { | ||||
| 
 | ||||
|             const v = tags["sidewalk"] | ||||
|             switch (v) { | ||||
|                 case "none": | ||||
|                 case "no": | ||||
|                     set("sidewalk:left", "no"); | ||||
|                     set("sidewalk:right", "no"); | ||||
|                     set("sidewalk:left", "no") | ||||
|                     set("sidewalk:right", "no") | ||||
|                     break | ||||
|                 case "both": | ||||
|                     set("sidewalk:left", "yes"); | ||||
|                     set("sidewalk:right", "yes"); | ||||
|                     break; | ||||
|                     set("sidewalk:left", "yes") | ||||
|                     set("sidewalk:right", "yes") | ||||
|                     break | ||||
|                 case "left": | ||||
|                     set("sidewalk:left", "yes"); | ||||
|                     set("sidewalk:right", "no"); | ||||
|                     break; | ||||
|                     set("sidewalk:left", "yes") | ||||
|                     set("sidewalk:right", "no") | ||||
|                     break | ||||
|                 case "right": | ||||
|                     set("sidewalk:left", "no"); | ||||
|                     set("sidewalk:right", "yes"); | ||||
|                     break; | ||||
|                     set("sidewalk:left", "no") | ||||
|                     set("sidewalk:right", "yes") | ||||
|                     break | ||||
|                 default: | ||||
|                     set("sidewalk:left", v); | ||||
|                     set("sidewalk:right", v); | ||||
|                     break; | ||||
|                     set("sidewalk:left", v) | ||||
|                     set("sidewalk:right", v) | ||||
|                     break | ||||
|             } | ||||
|             delete tags["sidewalk"] | ||||
|             somethingChanged = true | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const regex = /\([^:]*\):both:\(.*\)/ | ||||
|         for (const key in tags) { | ||||
|             const v = tags[key] | ||||
|  | @ -503,7 +538,6 @@ export default class SimpleMetaTaggers { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         return somethingChanged | ||||
|     } | ||||
| 
 | ||||
|  | @ -512,13 +546,16 @@ export default class SimpleMetaTaggers { | |||
|             new Combine([ | ||||
|                 "Metatags are extra tags available, in order to display more data or to give better questions.", | ||||
|                 "They are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.", | ||||
|                 "**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object" | ||||
|             ]).SetClass("flex-col") | ||||
| 
 | ||||
|         ]; | ||||
|                 "**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object", | ||||
|             ]).SetClass("flex-col"), | ||||
|         ] | ||||
| 
 | ||||
|         subElements.push(new Title("Metatags calculated by MapComplete", 2)) | ||||
|         subElements.push(new FixedUiElement("The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme")) | ||||
|         subElements.push( | ||||
|             new FixedUiElement( | ||||
|                 "The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme" | ||||
|             ) | ||||
|         ) | ||||
|         for (const metatag of SimpleMetaTaggers.metatags) { | ||||
|             subElements.push( | ||||
|                 new Title(metatag.keys.join(", "), 3), | ||||
|  | @ -529,5 +566,4 @@ export default class SimpleMetaTaggers { | |||
| 
 | ||||
|         return new Combine(subElements).SetClass("flex-col") | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,76 +1,80 @@ | |||
| import FeatureSwitchState from "./FeatureSwitchState"; | ||||
| import {ElementStorage} from "../ElementStorage"; | ||||
| import {Changes} from "../Osm/Changes"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import {BBox} from "../BBox"; | ||||
| import {QueryParameters} from "../Web/QueryParameters"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import ChangeToElementsActor from "../Actors/ChangeToElementsActor"; | ||||
| import PendingChangesUploader from "../Actors/PendingChangesUploader"; | ||||
| import FeatureSwitchState from "./FeatureSwitchState" | ||||
| import { ElementStorage } from "../ElementStorage" | ||||
| import { Changes } from "../Osm/Changes" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import Loc from "../../Models/Loc" | ||||
| import { BBox } from "../BBox" | ||||
| import { QueryParameters } from "../Web/QueryParameters" | ||||
| import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||
| import { Utils } from "../../Utils" | ||||
| import ChangeToElementsActor from "../Actors/ChangeToElementsActor" | ||||
| import PendingChangesUploader from "../Actors/PendingChangesUploader" | ||||
| 
 | ||||
| /** | ||||
|  * The part of the state keeping track of where the elements, loading them, configuring the feature pipeline etc | ||||
|  */ | ||||
| export default class ElementsState extends FeatureSwitchState { | ||||
| 
 | ||||
|     /** | ||||
|      The mapping from id -> UIEventSource<properties> | ||||
|      */ | ||||
|     public allElements: ElementStorage = new ElementStorage(); | ||||
|     public allElements: ElementStorage = new ElementStorage() | ||||
| 
 | ||||
|     /** | ||||
|      The latest element that was selected | ||||
|      */ | ||||
|     public readonly selectedElement = new UIEventSource<any>( | ||||
|         undefined, | ||||
|         "Selected element" | ||||
|     ); | ||||
| 
 | ||||
|     public readonly selectedElement = new UIEventSource<any>(undefined, "Selected element") | ||||
| 
 | ||||
|     /** | ||||
|      * The map location: currently centered lat, lon and zoom | ||||
|      */ | ||||
|     public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl"); | ||||
|     public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl") | ||||
| 
 | ||||
|     /** | ||||
|      * The current visible extent of the screen | ||||
|      */ | ||||
|     public readonly currentBounds = new UIEventSource<BBox>(undefined) | ||||
| 
 | ||||
| 
 | ||||
|     constructor(layoutToUse: LayoutConfig) { | ||||
|         super(layoutToUse); | ||||
|         super(layoutToUse) | ||||
| 
 | ||||
|          | ||||
|             function localStorageSynced(key: string, deflt: number, docs: string ): UIEventSource<number>{ | ||||
|         function localStorageSynced( | ||||
|             key: string, | ||||
|             deflt: number, | ||||
|             docs: string | ||||
|         ): UIEventSource<number> { | ||||
|             const localStorage = LocalStorageSource.Get(key) | ||||
|             const previousValue = localStorage.data | ||||
|             const src = UIEventSource.asFloat( | ||||
|                     QueryParameters.GetQueryParameter( | ||||
|                         key, | ||||
|                         "" + deflt, | ||||
|                         docs | ||||
|                     ).syncWith(localStorage) | ||||
|                 ); | ||||
|                 QueryParameters.GetQueryParameter(key, "" + deflt, docs).syncWith(localStorage) | ||||
|             ) | ||||
| 
 | ||||
|                 if(src.data === deflt){ | ||||
|             if (src.data === deflt) { | ||||
|                 const prev = Number(previousValue) | ||||
|                     if(!isNaN(prev)){ | ||||
|                 if (!isNaN(prev)) { | ||||
|                     src.setData(prev) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|                 return src; | ||||
|             return src | ||||
|         } | ||||
| 
 | ||||
|         // -- Location control initialization
 | ||||
|             const zoom = localStorageSynced("z",(layoutToUse?.startZoom ?? 1),"The initial/current zoom level") | ||||
|             const lat = localStorageSynced("lat",(layoutToUse?.startLat ?? 0),"The initial/current latitude") | ||||
|             const lon = localStorageSynced("lon",(layoutToUse?.startLon ?? 0),"The initial/current longitude of the app") | ||||
| 
 | ||||
|         const zoom = localStorageSynced( | ||||
|             "z", | ||||
|             layoutToUse?.startZoom ?? 1, | ||||
|             "The initial/current zoom level" | ||||
|         ) | ||||
|         const lat = localStorageSynced( | ||||
|             "lat", | ||||
|             layoutToUse?.startLat ?? 0, | ||||
|             "The initial/current latitude" | ||||
|         ) | ||||
|         const lon = localStorageSynced( | ||||
|             "lon", | ||||
|             layoutToUse?.startLon ?? 0, | ||||
|             "The initial/current longitude of the app" | ||||
|         ) | ||||
| 
 | ||||
|         this.locationControl.setData({ | ||||
|             zoom: Utils.asFloat(zoom.data), | ||||
|  | @ -79,11 +83,9 @@ export default class ElementsState extends FeatureSwitchState { | |||
|         }) | ||||
|         this.locationControl.addCallback((latlonz) => { | ||||
|             // Sync the location controls
 | ||||
|                 zoom.setData(latlonz.zoom); | ||||
|                 lat.setData(latlonz.lat); | ||||
|                 lon.setData(latlonz.lon); | ||||
|             }); | ||||
| 
 | ||||
|        | ||||
|             zoom.setData(latlonz.zoom) | ||||
|             lat.setData(latlonz.lat) | ||||
|             lon.setData(latlonz.lon) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | @ -1,37 +1,39 @@ | |||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import FeaturePipeline from "../FeatureSource/FeaturePipeline"; | ||||
| import {Tiles} from "../../Models/TileRange"; | ||||
| import ShowDataLayer from "../../UI/ShowDataLayer/ShowDataLayer"; | ||||
| import {TileHierarchyAggregator} from "../../UI/ShowDataLayer/TileHierarchyAggregator"; | ||||
| import ShowTileInfo from "../../UI/ShowDataLayer/ShowTileInfo"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import MapState from "./MapState"; | ||||
| import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"; | ||||
| import Hash from "../Web/Hash"; | ||||
| import {BBox} from "../BBox"; | ||||
| import FeatureInfoBox from "../../UI/Popup/FeatureInfoBox"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource"; | ||||
| import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator"; | ||||
| import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import FeaturePipeline from "../FeatureSource/FeaturePipeline" | ||||
| import { Tiles } from "../../Models/TileRange" | ||||
| import ShowDataLayer from "../../UI/ShowDataLayer/ShowDataLayer" | ||||
| import { TileHierarchyAggregator } from "../../UI/ShowDataLayer/TileHierarchyAggregator" | ||||
| import ShowTileInfo from "../../UI/ShowDataLayer/ShowTileInfo" | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import MapState from "./MapState" | ||||
| import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler" | ||||
| import Hash from "../Web/Hash" | ||||
| import { BBox } from "../BBox" | ||||
| import FeatureInfoBox from "../../UI/Popup/FeatureInfoBox" | ||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" | ||||
| import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator" | ||||
| import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| 
 | ||||
| export default class FeaturePipelineState extends MapState { | ||||
| 
 | ||||
|     /** | ||||
|      * The piece of code which fetches data from various sources and shows it on the background map | ||||
|      */ | ||||
|     public readonly featurePipeline: FeaturePipeline; | ||||
|     private readonly featureAggregator: TileHierarchyAggregator; | ||||
|     public readonly featurePipeline: FeaturePipeline | ||||
|     private readonly featureAggregator: TileHierarchyAggregator | ||||
|     private readonly metatagRecalculator: MetaTagRecalculator | ||||
|     private readonly popups : Map<string, ScrollableFullScreen> = new Map<string, ScrollableFullScreen>(); | ||||
|     private readonly popups: Map<string, ScrollableFullScreen> = new Map< | ||||
|         string, | ||||
|         ScrollableFullScreen | ||||
|     >() | ||||
| 
 | ||||
|     constructor(layoutToUse: LayoutConfig) { | ||||
|         super(layoutToUse); | ||||
|         super(layoutToUse) | ||||
| 
 | ||||
|         const clustering = layoutToUse?.clustering | ||||
|         this.featureAggregator = TileHierarchyAggregator.createHierarchy(this); | ||||
|         this.featureAggregator = TileHierarchyAggregator.createHierarchy(this) | ||||
|         const clusterCounter = this.featureAggregator | ||||
|         const self = this; | ||||
|         const self = this | ||||
| 
 | ||||
|         /** | ||||
|          * We are a bit in a bind: | ||||
|  | @ -52,25 +54,25 @@ export default class FeaturePipelineState extends MapState { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|          | ||||
|         function registerSource(source: FeatureSourceForLayer & Tiled) { | ||||
| 
 | ||||
|             clusterCounter.addTile(source) | ||||
|             const sourceBBox = source.features.map(allFeatures => BBox.bboxAroundAll(allFeatures.map(f => BBox.get(f.feature)))) | ||||
|             const sourceBBox = source.features.map((allFeatures) => | ||||
|                 BBox.bboxAroundAll(allFeatures.map((f) => BBox.get(f.feature))) | ||||
|             ) | ||||
| 
 | ||||
|             // Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering
 | ||||
|             const doShowFeatures = source.features.map( | ||||
|                 f => { | ||||
|                 (f) => { | ||||
|                     const z = self.locationControl.data.zoom | ||||
| 
 | ||||
|                     if (!source.layer.isDisplayed.data) { | ||||
|                         return false; | ||||
|                         return false | ||||
|                     } | ||||
| 
 | ||||
|                     const bounds = self.currentBounds.data | ||||
|                     if (bounds === undefined) { | ||||
|                         // Map is not yet displayed
 | ||||
|                         return false; | ||||
|                         return false | ||||
|                     } | ||||
| 
 | ||||
|                     if (!sourceBBox.data.overlapsWith(bounds)) { | ||||
|  | @ -78,10 +80,9 @@ export default class FeaturePipelineState extends MapState { | |||
|                         return false | ||||
|                     } | ||||
| 
 | ||||
| 
 | ||||
|                     if (z < source.layer.layerDef.minzoom) { | ||||
|                         // Layer is always hidden for this zoom level
 | ||||
|                         return false; | ||||
|                         return false | ||||
|                     } | ||||
| 
 | ||||
|                     if (z > clustering.maxZoom) { | ||||
|  | @ -93,54 +94,54 @@ export default class FeaturePipelineState extends MapState { | |||
|                         return false | ||||
|                     } | ||||
| 
 | ||||
|                     let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex); | ||||
|                     let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex) | ||||
|                     if (tileZ >= z) { | ||||
| 
 | ||||
|                         while (tileZ > z) { | ||||
|                             tileZ-- | ||||
|                             tileX = Math.floor(tileX / 2) | ||||
|                             tileY = Math.floor(tileY / 2) | ||||
|                         } | ||||
| 
 | ||||
|                         if (clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY))?.totalValue > clustering.minNeededElements) { | ||||
|                         if ( | ||||
|                             clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY)) | ||||
|                                 ?.totalValue > clustering.minNeededElements | ||||
|                         ) { | ||||
|                             // To much elements
 | ||||
|                             return false | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
| 
 | ||||
|                     return true | ||||
|                 }, [self.currentBounds, source.layer.isDisplayed, sourceBBox] | ||||
|                 }, | ||||
|                 [self.currentBounds, source.layer.isDisplayed, sourceBBox] | ||||
|             ) | ||||
| 
 | ||||
|             new ShowDataLayer( | ||||
|                 { | ||||
|             new ShowDataLayer({ | ||||
|                 features: source, | ||||
|                 leafletMap: self.leafletMap, | ||||
|                 layerToShow: source.layer.layerDef, | ||||
|                 doShowLayer: doShowFeatures, | ||||
|                 selectedElement: self.selectedElement, | ||||
|                 state: self, | ||||
|                     popup: (tags, layer) => self.CreatePopup(tags, layer) | ||||
|                 } | ||||
|             ) | ||||
|                 popup: (tags, layer) => self.CreatePopup(tags, layer), | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         this.featurePipeline = new FeaturePipeline(registerSource, this, {handleRawFeatureSource: registerRaw}); | ||||
|         this.featurePipeline = new FeaturePipeline(registerSource, this, { | ||||
|             handleRawFeatureSource: registerRaw, | ||||
|         }) | ||||
|         this.metatagRecalculator = new MetaTagRecalculator(this, this.featurePipeline) | ||||
|         this.metatagRecalculator.registerSource(this.currentView, true) | ||||
| 
 | ||||
|         sourcesToRegister.forEach(source => self.metatagRecalculator.registerSource(source)) | ||||
|         sourcesToRegister.forEach((source) => self.metatagRecalculator.registerSource(source)) | ||||
| 
 | ||||
|         new SelectedFeatureHandler(Hash.hash, this) | ||||
| 
 | ||||
|         this.AddClusteringToMap(this.leafletMap) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public CreatePopup(tags:UIEventSource<any> , layer: LayerConfig): ScrollableFullScreen{ | ||||
|         if(this.popups.has(tags.data.id)){ | ||||
|     public CreatePopup(tags: UIEventSource<any>, layer: LayerConfig): ScrollableFullScreen { | ||||
|         if (this.popups.has(tags.data.id)) { | ||||
|             return this.popups.get(tags.data.id) | ||||
|         } | ||||
|         const popup = new FeatureInfoBox(tags, layer, this) | ||||
|  | @ -155,15 +156,19 @@ export default class FeaturePipelineState extends MapState { | |||
|      */ | ||||
|     public AddClusteringToMap(leafletMap: UIEventSource<any>) { | ||||
|         const clustering = this.layoutToUse.clustering | ||||
|         const self = this; | ||||
|         const self = this | ||||
|         new ShowDataLayer({ | ||||
|             features: this.featureAggregator.getCountsForZoom(clustering, this.locationControl, clustering.minNeededElements), | ||||
|             features: this.featureAggregator.getCountsForZoom( | ||||
|                 clustering, | ||||
|                 this.locationControl, | ||||
|                 clustering.minNeededElements | ||||
|             ), | ||||
|             leafletMap: leafletMap, | ||||
|             layerToShow: ShowTileInfo.styling, | ||||
|             popup: this.featureSwitchIsDebugging.data ? (tags, layer) => new FeatureInfoBox(tags, layer, self) : undefined, | ||||
|             state: this | ||||
|             popup: this.featureSwitchIsDebugging.data | ||||
|                 ? (tags, layer) => new FeatureInfoBox(tags, layer, self) | ||||
|                 : undefined, | ||||
|             state: this, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,45 +1,43 @@ | |||
| /** | ||||
|  * The part of the global state which initializes the feature switches, based on default values and on the layoutToUse | ||||
|  */ | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {QueryParameters} from "../Web/QueryParameters"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import { QueryParameters } from "../Web/QueryParameters" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| export default class FeatureSwitchState { | ||||
| 
 | ||||
|     /** | ||||
|      * The layout that is being used in this run | ||||
|      */ | ||||
|     public readonly layoutToUse: LayoutConfig; | ||||
|     public readonly layoutToUse: LayoutConfig | ||||
| 
 | ||||
|     public readonly featureSwitchUserbadge: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchSearch: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchBackgroundSelection: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchAddNew: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchExtraLinkEnabled: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchMoreQuests: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchShareScreen: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchGeolocation: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchIsTesting: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchIsDebugging: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchApiURL: UIEventSource<string>; | ||||
|     public readonly featureSwitchFilter: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchEnableExport: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchFakeUser: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchExportAsPdf: UIEventSource<boolean>; | ||||
|     public readonly overpassUrl: UIEventSource<string[]>; | ||||
|     public readonly overpassTimeout: UIEventSource<number>; | ||||
|     public readonly overpassMaxZoom: UIEventSource<number>; | ||||
|     public readonly osmApiTileSize: UIEventSource<number>; | ||||
|     public readonly backgroundLayerId: UIEventSource<string>; | ||||
|     public readonly featureSwitchUserbadge: UIEventSource<boolean> | ||||
|     public readonly featureSwitchSearch: UIEventSource<boolean> | ||||
|     public readonly featureSwitchBackgroundSelection: UIEventSource<boolean> | ||||
|     public readonly featureSwitchAddNew: UIEventSource<boolean> | ||||
|     public readonly featureSwitchWelcomeMessage: UIEventSource<boolean> | ||||
|     public readonly featureSwitchExtraLinkEnabled: UIEventSource<boolean> | ||||
|     public readonly featureSwitchMoreQuests: UIEventSource<boolean> | ||||
|     public readonly featureSwitchShareScreen: UIEventSource<boolean> | ||||
|     public readonly featureSwitchGeolocation: UIEventSource<boolean> | ||||
|     public readonly featureSwitchIsTesting: UIEventSource<boolean> | ||||
|     public readonly featureSwitchIsDebugging: UIEventSource<boolean> | ||||
|     public readonly featureSwitchShowAllQuestions: UIEventSource<boolean> | ||||
|     public readonly featureSwitchApiURL: UIEventSource<string> | ||||
|     public readonly featureSwitchFilter: UIEventSource<boolean> | ||||
|     public readonly featureSwitchEnableExport: UIEventSource<boolean> | ||||
|     public readonly featureSwitchFakeUser: UIEventSource<boolean> | ||||
|     public readonly featureSwitchExportAsPdf: UIEventSource<boolean> | ||||
|     public readonly overpassUrl: UIEventSource<string[]> | ||||
|     public readonly overpassTimeout: UIEventSource<number> | ||||
|     public readonly overpassMaxZoom: UIEventSource<number> | ||||
|     public readonly osmApiTileSize: UIEventSource<number> | ||||
|     public readonly backgroundLayerId: UIEventSource<string> | ||||
| 
 | ||||
|     public constructor(layoutToUse: LayoutConfig) { | ||||
|         this.layoutToUse = layoutToUse; | ||||
| 
 | ||||
|         this.layoutToUse = layoutToUse | ||||
| 
 | ||||
|         // Helper function to initialize feature switches
 | ||||
|         function featSw( | ||||
|  | @ -47,104 +45,104 @@ export default class FeatureSwitchState { | |||
|             deflt: (layout: LayoutConfig) => boolean, | ||||
|             documentation: string | ||||
|         ): UIEventSource<boolean> { | ||||
| 
 | ||||
|             const defaultValue = deflt(layoutToUse); | ||||
|             const defaultValue = deflt(layoutToUse) | ||||
|             const queryParam = QueryParameters.GetQueryParameter( | ||||
|                 key, | ||||
|                 "" + defaultValue, | ||||
|                 documentation | ||||
|             ); | ||||
| 
 | ||||
|             // It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
 | ||||
|             return queryParam.sync((str) => | ||||
|                 str === undefined ? defaultValue : str !== "false", [], | ||||
|                 b => b == defaultValue ? undefined : (""+b) | ||||
|             ) | ||||
| 
 | ||||
|             // It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
 | ||||
|             return queryParam.sync( | ||||
|                 (str) => (str === undefined ? defaultValue : str !== "false"), | ||||
|                 [], | ||||
|                 (b) => (b == defaultValue ? undefined : "" + b) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         this.featureSwitchUserbadge = featSw( | ||||
|             "fs-userbadge", | ||||
|             (layoutToUse) => layoutToUse?.enableUserBadge ?? true, | ||||
|             "Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode." | ||||
|         ); | ||||
|         ) | ||||
|         this.featureSwitchSearch = featSw( | ||||
|             "fs-search", | ||||
|             (layoutToUse) => layoutToUse?.enableSearch ?? true, | ||||
|             "Disables/Enables the search bar" | ||||
|         ); | ||||
|         ) | ||||
|         this.featureSwitchBackgroundSelection = featSw( | ||||
|             "fs-background", | ||||
|             (layoutToUse) => layoutToUse?.enableBackgroundLayerSelection ?? true, | ||||
|             "Disables/Enables the background layer control" | ||||
|         ); | ||||
|         ) | ||||
| 
 | ||||
|         this.featureSwitchFilter = featSw( | ||||
|             "fs-filter", | ||||
|             (layoutToUse) => layoutToUse?.enableLayers ?? true, | ||||
|             "Disables/Enables the filter view" | ||||
|         ); | ||||
|         ) | ||||
|         this.featureSwitchAddNew = featSw( | ||||
|             "fs-add-new", | ||||
|             (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true, | ||||
|             "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)" | ||||
|         ); | ||||
|         ) | ||||
|         this.featureSwitchWelcomeMessage = featSw( | ||||
|             "fs-welcome-message", | ||||
|             () => true, | ||||
|             "Disables/enables the help menu or welcome message" | ||||
|         ); | ||||
|         ) | ||||
|         this.featureSwitchExtraLinkEnabled = featSw( | ||||
|             "fs-iframe-popout", | ||||
|             _ => true, | ||||
|             (_) => true, | ||||
|             "Disables/Enables the extraLink button. By default, if in iframe mode and the welcome message is hidden, a popout button to the full mapcomplete instance is shown instead (unless disabled with this switch or another extraLink button is enabled)" | ||||
|         ); | ||||
|         ) | ||||
|         this.featureSwitchMoreQuests = featSw( | ||||
|             "fs-more-quests", | ||||
|             (layoutToUse) => layoutToUse?.enableMoreQuests ?? true, | ||||
|             "Disables/Enables the 'More Quests'-tab in the welcome message" | ||||
|         ); | ||||
|         ) | ||||
|         this.featureSwitchShareScreen = featSw( | ||||
|             "fs-share-screen", | ||||
|             (layoutToUse) => layoutToUse?.enableShareScreen ?? true, | ||||
|             "Disables/Enables the 'Share-screen'-tab in the welcome message" | ||||
|         ); | ||||
|         ) | ||||
|         this.featureSwitchGeolocation = featSw( | ||||
|             "fs-geolocation", | ||||
|             (layoutToUse) => layoutToUse?.enableGeolocation ?? true, | ||||
|             "Disables/Enables the geolocation button" | ||||
|         ); | ||||
|         ) | ||||
|         this.featureSwitchShowAllQuestions = featSw( | ||||
|             "fs-all-questions", | ||||
|             (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false, | ||||
|             "Always show all questions" | ||||
|         ); | ||||
|         ) | ||||
| 
 | ||||
|         this.featureSwitchEnableExport = featSw( | ||||
|             "fs-export", | ||||
|             (layoutToUse) => layoutToUse?.enableExportButton ?? false, | ||||
|             "Enable the export as GeoJSON and CSV button" | ||||
|         ); | ||||
|         ) | ||||
|         this.featureSwitchExportAsPdf = featSw( | ||||
|             "fs-pdf", | ||||
|             (layoutToUse) => layoutToUse?.enablePdfDownload ?? false, | ||||
|             "Enable the PDF download button" | ||||
|         ); | ||||
|         ) | ||||
| 
 | ||||
|         this.featureSwitchApiURL = QueryParameters.GetQueryParameter( | ||||
|             "backend", | ||||
|             "osm", | ||||
|             "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'" | ||||
|         ); | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|         let testingDefaultValue = false; | ||||
|         if (this.featureSwitchApiURL.data !== "osm-test" && !Utils.runningFromConsole && | ||||
|             (location.hostname === "localhost" || location.hostname === "127.0.0.1")) { | ||||
|         let testingDefaultValue = false | ||||
|         if ( | ||||
|             this.featureSwitchApiURL.data !== "osm-test" && | ||||
|             !Utils.runningFromConsole && | ||||
|             (location.hostname === "localhost" || location.hostname === "127.0.0.1") | ||||
|         ) { | ||||
|             testingDefaultValue = true | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         this.featureSwitchIsTesting = QueryParameters.GetBooleanQueryParameter( | ||||
|             "test", | ||||
|             testingDefaultValue, | ||||
|  | @ -157,31 +155,47 @@ export default class FeatureSwitchState { | |||
|             "If true, shows some extra debugging help such as all the available tags on every object" | ||||
|         ) | ||||
| 
 | ||||
|         this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter("fake-user", false, | ||||
|             "If true, 'dryrun' mode is activated and a fake user account is loaded") | ||||
|         this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter( | ||||
|             "fake-user", | ||||
|             false, | ||||
|             "If true, 'dryrun' mode is activated and a fake user account is loaded" | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|         this.overpassUrl = QueryParameters.GetQueryParameter("overpassUrl", | ||||
|         this.overpassUrl = QueryParameters.GetQueryParameter( | ||||
|             "overpassUrl", | ||||
|             (layoutToUse?.overpassUrl ?? Constants.defaultOverpassUrls).join(","), | ||||
|             "Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter" | ||||
|         ).sync(param => param.split(","), [], urls => urls.join(",")) | ||||
|         ).sync( | ||||
|             (param) => param.split(","), | ||||
|             [], | ||||
|             (urls) => urls.join(",") | ||||
|         ) | ||||
| 
 | ||||
|         this.overpassTimeout = UIEventSource.asFloat(QueryParameters.GetQueryParameter("overpassTimeout", | ||||
|         this.overpassTimeout = UIEventSource.asFloat( | ||||
|             QueryParameters.GetQueryParameter( | ||||
|                 "overpassTimeout", | ||||
|                 "" + layoutToUse?.overpassTimeout, | ||||
|             "Set a different timeout (in seconds) for queries in overpass")) | ||||
|                 "Set a different timeout (in seconds) for queries in overpass" | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|         this.overpassMaxZoom = | ||||
|             UIEventSource.asFloat(QueryParameters.GetQueryParameter("overpassMaxZoom", | ||||
|         this.overpassMaxZoom = UIEventSource.asFloat( | ||||
|             QueryParameters.GetQueryParameter( | ||||
|                 "overpassMaxZoom", | ||||
|                 "" + layoutToUse?.overpassMaxZoom, | ||||
|                 " point to switch between OSM-api and overpass")) | ||||
|                 " point to switch between OSM-api and overpass" | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         this.osmApiTileSize = | ||||
|             UIEventSource.asFloat(QueryParameters.GetQueryParameter("osmApiTileSize", | ||||
|         this.osmApiTileSize = UIEventSource.asFloat( | ||||
|             QueryParameters.GetQueryParameter( | ||||
|                 "osmApiTileSize", | ||||
|                 "" + layoutToUse?.osmApiTileSize, | ||||
|                 "Tilesize when the OSM-API is used to fetch data within a BBOX")) | ||||
|                 "Tilesize when the OSM-API is used to fetch data within a BBOX" | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         this.featureSwitchUserbadge.addCallbackAndRun(userbadge => { | ||||
|         this.featureSwitchUserbadge.addCallbackAndRun((userbadge) => { | ||||
|             if (!userbadge) { | ||||
|                 this.featureSwitchAddNew.setData(false) | ||||
|             } | ||||
|  | @ -191,9 +205,6 @@ export default class FeatureSwitchState { | |||
|             "background", | ||||
|             layoutToUse?.defaultBackgroundId ?? "osm", | ||||
|             "The id of the background layer to start with" | ||||
|         ); | ||||
| 
 | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,34 +1,33 @@ | |||
| import UserRelatedState from "./UserRelatedState"; | ||||
| import {Store, Stores, UIEventSource} from "../UIEventSource"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import AvailableBaseLayers from "../Actors/AvailableBaseLayers"; | ||||
| import Attribution from "../../UI/BigComponents/Attribution"; | ||||
| import Minimap, {MinimapObj} from "../../UI/Base/Minimap"; | ||||
| import {Tiles} from "../../Models/TileRange"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; | ||||
| import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; | ||||
| import {QueryParameters} from "../Web/QueryParameters"; | ||||
| import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource"; | ||||
| import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| import {GeoOperations} from "../GeoOperations"; | ||||
| import TitleHandler from "../Actors/TitleHandler"; | ||||
| import {BBox} from "../BBox"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import {TiledStaticFeatureSource} from "../FeatureSource/Sources/StaticFeatureSource"; | ||||
| import {Translation, TypedTranslation} from "../../UI/i18n/Translation"; | ||||
| import {Tag} from "../Tags/Tag"; | ||||
| import {OsmConnection} from "../Osm/OsmConnection"; | ||||
| 
 | ||||
| import UserRelatedState from "./UserRelatedState" | ||||
| import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import BaseLayer from "../../Models/BaseLayer" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import AvailableBaseLayers from "../Actors/AvailableBaseLayers" | ||||
| import Attribution from "../../UI/BigComponents/Attribution" | ||||
| import Minimap, { MinimapObj } from "../../UI/Base/Minimap" | ||||
| import { Tiles } from "../../Models/TileRange" | ||||
| import BaseUIElement from "../../UI/BaseUIElement" | ||||
| import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" | ||||
| import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" | ||||
| import { QueryParameters } from "../Web/QueryParameters" | ||||
| import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer" | ||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" | ||||
| import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource" | ||||
| import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import TitleHandler from "../Actors/TitleHandler" | ||||
| import { BBox } from "../BBox" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import { TiledStaticFeatureSource } from "../FeatureSource/Sources/StaticFeatureSource" | ||||
| import { Translation, TypedTranslation } from "../../UI/i18n/Translation" | ||||
| import { Tag } from "../Tags/Tag" | ||||
| import { OsmConnection } from "../Osm/OsmConnection" | ||||
| 
 | ||||
| export interface GlobalFilter { | ||||
|     filter: FilterState, | ||||
|     id: string, | ||||
|     filter: FilterState | ||||
|     id: string | ||||
|     onNewPoint: { | ||||
|         safetyCheck: Translation, | ||||
|         safetyCheck: Translation | ||||
|         confirmAddNew: TypedTranslation<{ preset: Translation }> | ||||
|         tags: Tag[] | ||||
|     } | ||||
|  | @ -38,60 +37,64 @@ export interface GlobalFilter { | |||
|  * Contains all the leaflet-map related state | ||||
|  */ | ||||
| export default class MapState extends UserRelatedState { | ||||
| 
 | ||||
|     /** | ||||
|      The leaflet instance of the big basemap | ||||
|      */ | ||||
|     public leafletMap = new UIEventSource<any /*L.Map*/>(undefined, "leafletmap"); | ||||
|     public leafletMap = new UIEventSource<any /*L.Map*/>(undefined, "leafletmap") | ||||
|     /** | ||||
|      * A list of currently available background layers | ||||
|      */ | ||||
|     public availableBackgroundLayers: Store<BaseLayer[]>; | ||||
|     public availableBackgroundLayers: Store<BaseLayer[]> | ||||
| 
 | ||||
|     /** | ||||
|      * The current background layer | ||||
|      */ | ||||
|     public backgroundLayer: UIEventSource<BaseLayer>; | ||||
|     public backgroundLayer: UIEventSource<BaseLayer> | ||||
|     /** | ||||
|      * Last location where a click was registered | ||||
|      */ | ||||
|     public readonly LastClickLocation: UIEventSource<{ | ||||
|         lat: number; | ||||
|         lon: number; | ||||
|     }> = new UIEventSource<{ lat: number; lon: number }>(undefined); | ||||
|         lat: number | ||||
|         lon: number | ||||
|     }> = new UIEventSource<{ lat: number; lon: number }>(undefined) | ||||
| 
 | ||||
|     /** | ||||
|      * The bounds of the current map view | ||||
|      */ | ||||
|     public currentView: FeatureSourceForLayer & Tiled; | ||||
|     public currentView: FeatureSourceForLayer & Tiled | ||||
|     /** | ||||
|      * The location as delivered by the GPS | ||||
|      */ | ||||
|     public currentUserLocation: SimpleFeatureSource; | ||||
|     public currentUserLocation: SimpleFeatureSource | ||||
| 
 | ||||
|     /** | ||||
|      * All previously visited points | ||||
|      */ | ||||
|     public historicalUserLocations: SimpleFeatureSource; | ||||
|     public historicalUserLocations: SimpleFeatureSource | ||||
|     /** | ||||
|      * The number of seconds that the GPS-locations are stored in memory. | ||||
|      * Time in seconds | ||||
|      */ | ||||
|     public gpsLocationHistoryRetentionTime = new UIEventSource(7 * 24 * 60 * 60, "gps_location_retention") | ||||
|     public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled; | ||||
|     public gpsLocationHistoryRetentionTime = new UIEventSource( | ||||
|         7 * 24 * 60 * 60, | ||||
|         "gps_location_retention" | ||||
|     ) | ||||
|     public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled | ||||
| 
 | ||||
|     /** | ||||
|      * A feature source containing the current home location of the user | ||||
|      */ | ||||
|     public homeLocation: FeatureSourceForLayer & Tiled | ||||
| 
 | ||||
|     public readonly mainMapObject: BaseUIElement & MinimapObj; | ||||
| 
 | ||||
|     public readonly mainMapObject: BaseUIElement & MinimapObj | ||||
| 
 | ||||
|     /** | ||||
|      * Which layers are enabled in the current theme and what filters are applied onto them | ||||
|      */ | ||||
|     public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers"); | ||||
|     public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>( | ||||
|         [], | ||||
|         "filteredLayers" | ||||
|     ) | ||||
| 
 | ||||
|     /** | ||||
|      * Filters which apply onto all layers | ||||
|  | @ -101,31 +104,30 @@ export default class MapState extends UserRelatedState { | |||
|     /** | ||||
|      * Which overlays are shown | ||||
|      */ | ||||
|     public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[] | ||||
| 
 | ||||
|     public overlayToggles: { config: TilesourceConfig; isDisplayed: UIEventSource<boolean> }[] | ||||
| 
 | ||||
|     constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { | ||||
|         super(layoutToUse, options); | ||||
|         super(layoutToUse, options) | ||||
| 
 | ||||
|         this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl); | ||||
|         this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl) | ||||
| 
 | ||||
|         let defaultLayer = AvailableBaseLayers.osmCarto | ||||
|         const available = this.availableBackgroundLayers.data; | ||||
|         const available = this.availableBackgroundLayers.data | ||||
|         for (const layer of available) { | ||||
|             if (this.backgroundLayerId.data === layer.id) { | ||||
|                 defaultLayer = layer; | ||||
|                 defaultLayer = layer | ||||
|             } | ||||
|         } | ||||
|         const self = this | ||||
|         this.backgroundLayer = new UIEventSource<BaseLayer>(defaultLayer) | ||||
|         this.backgroundLayer.addCallbackAndRunD(layer => self.backgroundLayerId.setData(layer.id)) | ||||
|         this.backgroundLayer.addCallbackAndRunD((layer) => self.backgroundLayerId.setData(layer.id)) | ||||
| 
 | ||||
|         const attr = new Attribution( | ||||
|             this.locationControl, | ||||
|             this.osmConnection.userDetails, | ||||
|             this.layoutToUse, | ||||
|             this.currentBounds | ||||
|         ); | ||||
|         ) | ||||
| 
 | ||||
|         // Will write into this.leafletMap
 | ||||
|         this.mainMapObject = Minimap.createMiniMap({ | ||||
|  | @ -134,18 +136,23 @@ export default class MapState extends UserRelatedState { | |||
|             leafletMap: this.leafletMap, | ||||
|             bounds: this.currentBounds, | ||||
|             attribution: attr, | ||||
|             lastClickLocation: this.LastClickLocation | ||||
|             lastClickLocation: this.LastClickLocation, | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         this.overlayToggles = this.layoutToUse?.tileLayerSources | ||||
|             ?.filter(c => c.name !== undefined) | ||||
|             ?.map(c => ({ | ||||
|         this.overlayToggles = | ||||
|             this.layoutToUse?.tileLayerSources | ||||
|                 ?.filter((c) => c.name !== undefined) | ||||
|                 ?.map((c) => ({ | ||||
|                     config: c, | ||||
|                 isDisplayed: QueryParameters.GetBooleanQueryParameter("overlay-" + c.id, c.defaultState, "Wether or not the overlay " + c.id + " is shown") | ||||
|                     isDisplayed: QueryParameters.GetBooleanQueryParameter( | ||||
|                         "overlay-" + c.id, | ||||
|                         c.defaultState, | ||||
|                         "Wether or not the overlay " + c.id + " is shown" | ||||
|                     ), | ||||
|                 })) ?? [] | ||||
|         this.filteredLayers = new UIEventSource<FilteredLayer[]>( MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection)) | ||||
| 
 | ||||
|         this.filteredLayers = new UIEventSource<FilteredLayer[]>( | ||||
|             MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection) | ||||
|         ) | ||||
| 
 | ||||
|         this.lockBounds() | ||||
|         this.AddAllOverlaysToMap(this.leafletMap) | ||||
|  | @ -155,7 +162,7 @@ export default class MapState extends UserRelatedState { | |||
|         this.initUserLocationTrail() | ||||
|         this.initCurrentView() | ||||
| 
 | ||||
|         new TitleHandler(this); | ||||
|         new TitleHandler(this) | ||||
|     } | ||||
| 
 | ||||
|     public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) { | ||||
|  | @ -171,15 +178,14 @@ export default class MapState extends UserRelatedState { | |||
|             } | ||||
|             new ShowOverlayLayer(tileLayerSource, leafletMap) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private lockBounds() { | ||||
|         const layout = this.layoutToUse; | ||||
|         const layout = this.layoutToUse | ||||
|         if (!layout?.lockLocation) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         console.warn("Locking the bounds to ", layout.lockLocation); | ||||
|         console.warn("Locking the bounds to ", layout.lockLocation) | ||||
|         this.mainMapObject.installBounds( | ||||
|             new BBox(layout.lockLocation), | ||||
|             this.featureSwitchIsTesting.data | ||||
|  | @ -187,17 +193,19 @@ export default class MapState extends UserRelatedState { | |||
|     } | ||||
| 
 | ||||
|     private initCurrentView() { | ||||
|         let currentViewLayer: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "current_view")[0] | ||||
|         let currentViewLayer: FilteredLayer = this.filteredLayers.data.filter( | ||||
|             (l) => l.layerDef.id === "current_view" | ||||
|         )[0] | ||||
| 
 | ||||
|         if (currentViewLayer === undefined) { | ||||
|             // This layer is not needed by the theme and thus unloaded
 | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         let i = 0 | ||||
|         const self = this; | ||||
|         const features: Store<{ feature: any, freshness: Date }[]> = this.currentBounds.map(bounds => { | ||||
|         const self = this | ||||
|         const features: Store<{ feature: any; freshness: Date }[]> = this.currentBounds.map( | ||||
|             (bounds) => { | ||||
|                 if (bounds === undefined) { | ||||
|                     return [] | ||||
|                 } | ||||
|  | @ -208,48 +216,59 @@ export default class MapState extends UserRelatedState { | |||
|                         type: "Feature", | ||||
|                         properties: { | ||||
|                             id: "current_view-" + i, | ||||
|                         "current_view": "yes", | ||||
|                         "zoom": "" + self.locationControl.data.zoom | ||||
|                             current_view: "yes", | ||||
|                             zoom: "" + self.locationControl.data.zoom, | ||||
|                         }, | ||||
|                         geometry: { | ||||
|                             type: "Polygon", | ||||
|                         coordinates: [[ | ||||
|                             coordinates: [ | ||||
|                                 [ | ||||
|                                     [bounds.maxLon, bounds.maxLat], | ||||
|                                     [bounds.minLon, bounds.maxLat], | ||||
|                                     [bounds.minLon, bounds.minLat], | ||||
|                                     [bounds.maxLon, bounds.minLat], | ||||
|                                     [bounds.maxLon, bounds.maxLat], | ||||
|                         ]] | ||||
|                     } | ||||
|                 } | ||||
|                                 ], | ||||
|                             ], | ||||
|                         }, | ||||
|                     }, | ||||
|                 } | ||||
|                 return [feature] | ||||
|         }) | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
|         this.currentView = new TiledStaticFeatureSource(features, currentViewLayer); | ||||
|         this.currentView = new TiledStaticFeatureSource(features, currentViewLayer) | ||||
|     } | ||||
| 
 | ||||
|     private initGpsLocation() { | ||||
|         // Initialize the gps layer data. This is emtpy for now, the actual writing happens in the Geolocationhandler
 | ||||
|         let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_location")[0] | ||||
|         let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter( | ||||
|             (l) => l.layerDef.id === "gps_location" | ||||
|         )[0] | ||||
|         if (gpsLayerDef === undefined) { | ||||
|             return | ||||
|         } | ||||
|         this.currentUserLocation = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0)); | ||||
|         this.currentUserLocation = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0)) | ||||
|     } | ||||
| 
 | ||||
|     private initUserLocationTrail() { | ||||
|         const features = LocalStorageSource.GetParsed<{ feature: any, freshness: Date }[]>("gps_location_history", []) | ||||
|         const features = LocalStorageSource.GetParsed<{ feature: any; freshness: Date }[]>( | ||||
|             "gps_location_history", | ||||
|             [] | ||||
|         ) | ||||
|         const now = new Date().getTime() | ||||
|         features.data = features.data | ||||
|             .map(ff => ({feature: ff.feature, freshness: new Date(ff.freshness)})) | ||||
|             .filter(ff => (now - ff.freshness.getTime()) < 1000 * this.gpsLocationHistoryRetentionTime.data) | ||||
|             .map((ff) => ({ feature: ff.feature, freshness: new Date(ff.freshness) })) | ||||
|             .filter( | ||||
|                 (ff) => | ||||
|                     now - ff.freshness.getTime() < 1000 * this.gpsLocationHistoryRetentionTime.data | ||||
|             ) | ||||
|         features.ping() | ||||
|         const self = this; | ||||
|         const self = this | ||||
|         let i = 0 | ||||
|         this.currentUserLocation?.features?.addCallbackAndRunD(([location]) => { | ||||
|             if (location === undefined) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             const previousLocation = features.data[features.data.length - 1] | ||||
|  | @ -261,30 +280,37 @@ export default class MapState extends UserRelatedState { | |||
|                 let timeDiff = Number.MAX_VALUE // in seconds
 | ||||
|                 const olderLocation = features.data[features.data.length - 2] | ||||
|                 if (olderLocation !== undefined) { | ||||
|                     timeDiff = (new Date(previousLocation.freshness).getTime() - new Date(olderLocation.freshness).getTime()) / 1000 | ||||
|                     timeDiff = | ||||
|                         (new Date(previousLocation.freshness).getTime() - | ||||
|                             new Date(olderLocation.freshness).getTime()) / | ||||
|                         1000 | ||||
|                 } | ||||
|                 if (d < 20 && timeDiff < 60) { | ||||
|                     // Do not append changes less then 20m - it's probably noise anyway
 | ||||
|                     return; | ||||
|                     return | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             const feature = JSON.parse(JSON.stringify(location.feature)) | ||||
|             feature.properties.id = "gps/" + features.data.length | ||||
|             i++ | ||||
|             features.data.push({feature, freshness: new Date()}) | ||||
|             features.data.push({ feature, freshness: new Date() }) | ||||
|             features.ping() | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_location_history")[0] | ||||
|         let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter( | ||||
|             (l) => l.layerDef.id === "gps_location_history" | ||||
|         )[0] | ||||
|         if (gpsLayerDef !== undefined) { | ||||
|             this.historicalUserLocations = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0), features); | ||||
|             this.historicalUserLocations = new SimpleFeatureSource( | ||||
|                 gpsLayerDef, | ||||
|                 Tiles.tile_index(0, 0, 0), | ||||
|                 features | ||||
|             ) | ||||
|             this.changes.setHistoricalUserLocations(this.historicalUserLocations) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const asLine = features.map(allPoints => { | ||||
|         const asLine = features.map((allPoints) => { | ||||
|             if (allPoints === undefined || allPoints.length < 2) { | ||||
|                 return [] | ||||
|             } | ||||
|  | @ -292,136 +318,184 @@ export default class MapState extends UserRelatedState { | |||
|             const feature = { | ||||
|                 type: "Feature", | ||||
|                 properties: { | ||||
|                     "id": "location_track", | ||||
|                     id: "location_track", | ||||
|                     "_date:now": new Date().toISOString(), | ||||
|                 }, | ||||
|                 geometry: { | ||||
|                     type: "LineString", | ||||
|                     coordinates: allPoints.map(ff => ff.feature.geometry.coordinates) | ||||
|                 } | ||||
|                     coordinates: allPoints.map((ff) => ff.feature.geometry.coordinates), | ||||
|                 }, | ||||
|             } | ||||
| 
 | ||||
|             self.allElements.ContainingFeatures.set(feature.properties.id, feature) | ||||
| 
 | ||||
|             return [{ | ||||
|             return [ | ||||
|                 { | ||||
|                     feature, | ||||
|                 freshness: new Date() | ||||
|             }] | ||||
|                     freshness: new Date(), | ||||
|                 }, | ||||
|             ] | ||||
|         }) | ||||
|         let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_track")[0] | ||||
|         let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter( | ||||
|             (l) => l.layerDef.id === "gps_track" | ||||
|         )[0] | ||||
|         if (gpsLineLayerDef !== undefined) { | ||||
|             this.historicalUserLocationsTrack = new TiledStaticFeatureSource(asLine, gpsLineLayerDef); | ||||
|             this.historicalUserLocationsTrack = new TiledStaticFeatureSource( | ||||
|                 asLine, | ||||
|                 gpsLineLayerDef | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private initHomeLocation() { | ||||
|         const empty = [] | ||||
|         const feature = Stores.ListStabilized(this.osmConnection.userDetails.map(userDetails => { | ||||
| 
 | ||||
|         const feature = Stores.ListStabilized( | ||||
|             this.osmConnection.userDetails.map((userDetails) => { | ||||
|                 if (userDetails === undefined) { | ||||
|                 return undefined; | ||||
|                     return undefined | ||||
|                 } | ||||
|             const home = userDetails.home; | ||||
|                 const home = userDetails.home | ||||
|                 if (home === undefined) { | ||||
|                 return undefined; | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return [home.lon, home.lat] | ||||
|         })).map(homeLonLat => { | ||||
|             }) | ||||
|         ).map((homeLonLat) => { | ||||
|             if (homeLonLat === undefined) { | ||||
|                 return empty | ||||
|             } | ||||
|             return [{ | ||||
|             return [ | ||||
|                 { | ||||
|                     feature: { | ||||
|                     "type": "Feature", | ||||
|                     "properties": { | ||||
|                         "id": "home", | ||||
|                         type: "Feature", | ||||
|                         properties: { | ||||
|                             id: "home", | ||||
|                             "user:home": "yes", | ||||
|                         "_lon": homeLonLat[0], | ||||
|                         "_lat": homeLonLat[1] | ||||
|                             _lon: homeLonLat[0], | ||||
|                             _lat: homeLonLat[1], | ||||
|                         }, | ||||
|                     "geometry": { | ||||
|                         "type": "Point", | ||||
|                         "coordinates": homeLonLat | ||||
|                     } | ||||
|                 }, freshness: new Date() | ||||
|             }] | ||||
|                         geometry: { | ||||
|                             type: "Point", | ||||
|                             coordinates: homeLonLat, | ||||
|                         }, | ||||
|                     }, | ||||
|                     freshness: new Date(), | ||||
|                 }, | ||||
|             ] | ||||
|         }) | ||||
| 
 | ||||
|         const flayer = this.filteredLayers.data.filter(l => l.layerDef.id === "home_location")[0] | ||||
|         const flayer = this.filteredLayers.data.filter((l) => l.layerDef.id === "home_location")[0] | ||||
|         if (flayer !== undefined) { | ||||
|             this.homeLocation = new TiledStaticFeatureSource(feature, flayer) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private static getPref(osmConnection: OsmConnection, key: string, layer: LayerConfig): UIEventSource<boolean> { | ||||
|         return osmConnection | ||||
|             .GetPreference(key, layer.shownByDefault + "") | ||||
|             .sync(v => { | ||||
|     private static getPref( | ||||
|         osmConnection: OsmConnection, | ||||
|         key: string, | ||||
|         layer: LayerConfig | ||||
|     ): UIEventSource<boolean> { | ||||
|         return osmConnection.GetPreference(key, layer.shownByDefault + "").sync( | ||||
|             (v) => { | ||||
|                 if (v === undefined) { | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return v === "true"; | ||||
|             }, [], b => { | ||||
|                 return v === "true" | ||||
|             }, | ||||
|             [], | ||||
|             (b) => { | ||||
|                 if (b === undefined) { | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return "" + b; | ||||
|             }) | ||||
|                 return "" + b | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     public static InitializeFilteredLayers(layoutToUse: {layers: LayerConfig[], id: string}, osmConnection: OsmConnection): FilteredLayer[] { | ||||
|     public static InitializeFilteredLayers( | ||||
|         layoutToUse: { layers: LayerConfig[]; id: string }, | ||||
|         osmConnection: OsmConnection | ||||
|     ): FilteredLayer[] { | ||||
|         if (layoutToUse === undefined) { | ||||
|             return [] | ||||
|         } | ||||
|         const flayers: FilteredLayer[] = []; | ||||
|         const flayers: FilteredLayer[] = [] | ||||
|         for (const layer of layoutToUse.layers) { | ||||
|             let isDisplayed: UIEventSource<boolean> | ||||
|             if (layer.syncSelection === "local") { | ||||
|                 isDisplayed = LocalStorageSource.GetParsed(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer.shownByDefault) | ||||
|                 isDisplayed = LocalStorageSource.GetParsed( | ||||
|                     layoutToUse.id + "-layer-" + layer.id + "-enabled", | ||||
|                     layer.shownByDefault | ||||
|                 ) | ||||
|             } else if (layer.syncSelection === "theme-only") { | ||||
|                 isDisplayed = MapState.getPref(osmConnection, layoutToUse.id + "-layer-" + layer.id + "-enabled", layer) | ||||
|                 isDisplayed = MapState.getPref( | ||||
|                     osmConnection, | ||||
|                     layoutToUse.id + "-layer-" + layer.id + "-enabled", | ||||
|                     layer | ||||
|                 ) | ||||
|             } else if (layer.syncSelection === "global") { | ||||
|                 isDisplayed = MapState.getPref(osmConnection,"layer-" + layer.id + "-enabled", layer) | ||||
|                 isDisplayed = MapState.getPref( | ||||
|                     osmConnection, | ||||
|                     "layer-" + layer.id + "-enabled", | ||||
|                     layer | ||||
|                 ) | ||||
|             } else { | ||||
|                 isDisplayed = QueryParameters.GetBooleanQueryParameter("layer-" + layer.id, layer.shownByDefault, "Wether or not layer " + layer.id + " is shown") | ||||
|                 isDisplayed = QueryParameters.GetBooleanQueryParameter( | ||||
|                     "layer-" + layer.id, | ||||
|                     layer.shownByDefault, | ||||
|                     "Wether or not layer " + layer.id + " is shown" | ||||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|             const flayer: FilteredLayer = { | ||||
|                 isDisplayed, | ||||
|                 layerDef: layer, | ||||
|                 appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>()) | ||||
|             }; | ||||
|             layer.filters.forEach(filterConfig => { | ||||
|                 appliedFilters: new UIEventSource<Map<string, FilterState>>( | ||||
|                     new Map<string, FilterState>() | ||||
|                 ), | ||||
|             } | ||||
|             layer.filters.forEach((filterConfig) => { | ||||
|                 const stateSrc = filterConfig.initState() | ||||
| 
 | ||||
|                 stateSrc.addCallbackAndRun(state => flayer.appliedFilters.data.set(filterConfig.id, state)) | ||||
|                 flayer.appliedFilters.map(dict => dict.get(filterConfig.id)) | ||||
|                     .addCallback(state => stateSrc.setData(state)) | ||||
|                 stateSrc.addCallbackAndRun((state) => | ||||
|                     flayer.appliedFilters.data.set(filterConfig.id, state) | ||||
|                 ) | ||||
|                 flayer.appliedFilters | ||||
|                     .map((dict) => dict.get(filterConfig.id)) | ||||
|                     .addCallback((state) => stateSrc.setData(state)) | ||||
|             }) | ||||
| 
 | ||||
|             flayers.push(flayer); | ||||
|             flayers.push(flayer) | ||||
|         } | ||||
| 
 | ||||
|         for (const layer of layoutToUse.layers) { | ||||
|             if (layer.filterIsSameAs === undefined) { | ||||
|                 continue | ||||
|             } | ||||
|             const toReuse = flayers.find(l => l.layerDef.id === layer.filterIsSameAs) | ||||
|             const toReuse = flayers.find((l) => l.layerDef.id === layer.filterIsSameAs) | ||||
|             if (toReuse === undefined) { | ||||
|                 throw "Error in layer " + layer.id + ": it defines that it should be use the filters of " + layer.filterIsSameAs + ", but this layer was not loaded" | ||||
|                 throw ( | ||||
|                     "Error in layer " + | ||||
|                     layer.id + | ||||
|                     ": it defines that it should be use the filters of " + | ||||
|                     layer.filterIsSameAs + | ||||
|                     ", but this layer was not loaded" | ||||
|                 ) | ||||
|             } | ||||
|             console.warn("Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs) | ||||
|             const selfLayer = flayers.findIndex(l => l.layerDef.id === layer.id) | ||||
|             console.warn( | ||||
|                 "Linking filter and isDisplayed-states of " + | ||||
|                     layer.id + | ||||
|                     " and " + | ||||
|                     layer.filterIsSameAs | ||||
|             ) | ||||
|             const selfLayer = flayers.findIndex((l) => l.layerDef.id === layer.id) | ||||
|             flayers[selfLayer] = { | ||||
|                 isDisplayed: toReuse.isDisplayed, | ||||
|                 layerDef: layer, | ||||
|                 appliedFilters: toReuse.appliedFilters | ||||
|             }; | ||||
|                 appliedFilters: toReuse.appliedFilters, | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return flayers; | ||||
|         return flayers | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,50 +1,48 @@ | |||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {OsmConnection} from "../Osm/OsmConnection"; | ||||
| import {MangroveIdentity} from "../Web/MangroveReviews"; | ||||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import {QueryParameters} from "../Web/QueryParameters"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import Locale from "../../UI/i18n/Locale"; | ||||
| import ElementsState from "./ElementsState"; | ||||
| import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater"; | ||||
| import {Changes} from "../Osm/Changes"; | ||||
| import ChangeToElementsActor from "../Actors/ChangeToElementsActor"; | ||||
| import PendingChangesUploader from "../Actors/PendingChangesUploader"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import { OsmConnection } from "../Osm/OsmConnection" | ||||
| import { MangroveIdentity } from "../Web/MangroveReviews" | ||||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import { QueryParameters } from "../Web/QueryParameters" | ||||
| import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||
| import { Utils } from "../../Utils" | ||||
| import Locale from "../../UI/i18n/Locale" | ||||
| import ElementsState from "./ElementsState" | ||||
| import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater" | ||||
| import { Changes } from "../Osm/Changes" | ||||
| import ChangeToElementsActor from "../Actors/ChangeToElementsActor" | ||||
| import PendingChangesUploader from "../Actors/PendingChangesUploader" | ||||
| import * as translators from "../../assets/translators.json" | ||||
| import Maproulette from "../Maproulette"; | ||||
| import Maproulette from "../Maproulette" | ||||
| 
 | ||||
| /** | ||||
|  * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, | ||||
|  * which layers they enabled, ... | ||||
|  */ | ||||
| export default class UserRelatedState extends ElementsState { | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      The user credentials | ||||
|      */ | ||||
|     public osmConnection: OsmConnection; | ||||
|     public osmConnection: OsmConnection | ||||
|     /** | ||||
|      THe change handler | ||||
|      */ | ||||
|     public changes: Changes; | ||||
|     public changes: Changes | ||||
|     /** | ||||
|      * The key for mangrove | ||||
|      */ | ||||
|     public mangroveIdentity: MangroveIdentity; | ||||
|     public mangroveIdentity: MangroveIdentity | ||||
| 
 | ||||
|     /** | ||||
|      * Maproulette connection | ||||
|      */ | ||||
|     public maprouletteConnection: Maproulette; | ||||
|     public maprouletteConnection: Maproulette | ||||
| 
 | ||||
|     public readonly isTranslator : Store<boolean>; | ||||
|     public readonly isTranslator: Store<boolean> | ||||
| 
 | ||||
|     public readonly installedUserThemes: Store<string[]> | ||||
| 
 | ||||
|     constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { | ||||
|         super(layoutToUse); | ||||
|         super(layoutToUse) | ||||
| 
 | ||||
|         this.osmConnection = new OsmConnection({ | ||||
|             dryRun: this.featureSwitchIsTesting, | ||||
|  | @ -54,78 +52,82 @@ export default class UserRelatedState extends ElementsState { | |||
|                 undefined, | ||||
|                 "Used to complete the login" | ||||
|             ), | ||||
|             osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data, | ||||
|             attemptLogin: options?.attemptLogin | ||||
|             osmConfiguration: <"osm" | "osm-test">this.featureSwitchApiURL.data, | ||||
|             attemptLogin: options?.attemptLogin, | ||||
|         }) | ||||
|         const translationMode  = this.osmConnection.GetPreference("translation-mode").sync(str => str === undefined ? undefined : str === "true", [], b => b === undefined ? undefined : b+"") | ||||
|         const translationMode = this.osmConnection.GetPreference("translation-mode").sync( | ||||
|             (str) => (str === undefined ? undefined : str === "true"), | ||||
|             [], | ||||
|             (b) => (b === undefined ? undefined : b + "") | ||||
|         ) | ||||
| 
 | ||||
|         translationMode.syncWith(Locale.showLinkToWeblate) | ||||
| 
 | ||||
|         this.isTranslator = this.osmConnection.userDetails.map(ud => { | ||||
|             if(!ud.loggedIn){ | ||||
|                 return false; | ||||
|         this.isTranslator = this.osmConnection.userDetails.map((ud) => { | ||||
|             if (!ud.loggedIn) { | ||||
|                 return false | ||||
|             } | ||||
|             const name= ud.name.toLowerCase().replace(/\s+/g, '') | ||||
|             return translators.contributors.some(c => c.contributor.toLowerCase().replace(/\s+/g, '') === name) | ||||
|             const name = ud.name.toLowerCase().replace(/\s+/g, "") | ||||
|             return translators.contributors.some( | ||||
|                 (c) => c.contributor.toLowerCase().replace(/\s+/g, "") === name | ||||
|             ) | ||||
|         }) | ||||
| 
 | ||||
|         this.isTranslator.addCallbackAndRunD(ud => { | ||||
|             if(ud){ | ||||
|         this.isTranslator.addCallbackAndRunD((ud) => { | ||||
|             if (ud) { | ||||
|                 Locale.showLinkToWeblate.setData(true) | ||||
|             } | ||||
|         }); | ||||
|         }) | ||||
| 
 | ||||
|         this.changes = new Changes(this, layoutToUse?.isLeftRightSensitive() ?? false) | ||||
| 
 | ||||
| 
 | ||||
|         new ChangeToElementsActor(this.changes, this.allElements) | ||||
|         new PendingChangesUploader(this.changes, this.selectedElement); | ||||
|         new PendingChangesUploader(this.changes, this.selectedElement) | ||||
| 
 | ||||
|         this.mangroveIdentity = new MangroveIdentity( | ||||
|             this.osmConnection.GetLongPreference("identity", "mangrove") | ||||
|         ); | ||||
|         ) | ||||
| 
 | ||||
|         this.maprouletteConnection = new Maproulette(); | ||||
|         this.maprouletteConnection = new Maproulette() | ||||
| 
 | ||||
|         if (layoutToUse?.hideFromOverview) { | ||||
|             this.osmConnection.isLoggedIn.addCallbackAndRunD(loggedIn => { | ||||
|             this.osmConnection.isLoggedIn.addCallbackAndRunD((loggedIn) => { | ||||
|                 if (loggedIn) { | ||||
|                     this.osmConnection | ||||
|                         .GetPreference("hidden-theme-" + layoutToUse?.id + "-enabled") | ||||
|                         .setData("true"); | ||||
|                     return true; | ||||
|                         .setData("true") | ||||
|                     return true | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         if (this.layoutToUse !== undefined && !this.layoutToUse.official) { | ||||
|             console.log("Marking unofficial theme as visited") | ||||
|             this.osmConnection.GetLongPreference("unofficial-theme-" + this.layoutToUse.id) | ||||
|                 .setData(JSON.stringify({ | ||||
|             this.osmConnection.GetLongPreference("unofficial-theme-" + this.layoutToUse.id).setData( | ||||
|                 JSON.stringify({ | ||||
|                     id: this.layoutToUse.id, | ||||
|                     icon: this.layoutToUse.icon, | ||||
|                     title: this.layoutToUse.title.translations, | ||||
|                     shortDescription: this.layoutToUse.shortDescription.translations, | ||||
|                     definition: this.layoutToUse["definition"] | ||||
|                 })) | ||||
|                     definition: this.layoutToUse["definition"], | ||||
|                 }) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         this.InitializeLanguage(); | ||||
|         this.InitializeLanguage() | ||||
|         new SelectedElementTagsUpdater(this) | ||||
|         this.installedUserThemes = this.InitInstalledUserThemes(); | ||||
| 
 | ||||
|         this.installedUserThemes = this.InitInstalledUserThemes() | ||||
|     } | ||||
| 
 | ||||
|     private InitializeLanguage() { | ||||
|         const layoutToUse = this.layoutToUse; | ||||
|         Locale.language.syncWith(this.osmConnection.GetPreference("language")); | ||||
|         Locale.language | ||||
|             .addCallback((currentLanguage) => { | ||||
|         const layoutToUse = this.layoutToUse | ||||
|         Locale.language.syncWith(this.osmConnection.GetPreference("language")) | ||||
|         Locale.language.addCallback((currentLanguage) => { | ||||
|             if (layoutToUse === undefined) { | ||||
|                     return; | ||||
|                 return | ||||
|             } | ||||
|                 if(Locale.showLinkToWeblate.data){ | ||||
|                     return true; // Disable auto switching as we are in translators mode
 | ||||
|             if (Locale.showLinkToWeblate.data) { | ||||
|                 return true // Disable auto switching as we are in translators mode
 | ||||
|             } | ||||
|             if (this.layoutToUse.language.indexOf(currentLanguage) < 0) { | ||||
|                 console.log( | ||||
|  | @ -134,34 +136,36 @@ export default class UserRelatedState extends ElementsState { | |||
|                     "as", | ||||
|                     currentLanguage, | ||||
|                     " is unsupported" | ||||
|                     ); | ||||
|                 ) | ||||
|                 // The current language is not supported -> switch to a supported one
 | ||||
|                     Locale.language.setData(layoutToUse.language[0]); | ||||
|                 Locale.language.setData(layoutToUse.language[0]) | ||||
|             } | ||||
|         }) | ||||
|         Locale.language.ping(); | ||||
|         Locale.language.ping() | ||||
|     } | ||||
| 
 | ||||
|     private InitInstalledUserThemes(): Store<string[]>{ | ||||
|         const prefix = "mapcomplete-unofficial-theme-"; | ||||
|     private InitInstalledUserThemes(): Store<string[]> { | ||||
|         const prefix = "mapcomplete-unofficial-theme-" | ||||
|         const postfix = "-combined-length" | ||||
|         return this.osmConnection.preferencesHandler.preferences.map(prefs => | ||||
|         return this.osmConnection.preferencesHandler.preferences.map((prefs) => | ||||
|             Object.keys(prefs) | ||||
|                 .filter(k => k.startsWith(prefix) && k.endsWith(postfix)) | ||||
|                 .map(k => k.substring(prefix.length, k.length - postfix.length)) | ||||
|                 .filter((k) => k.startsWith(prefix) && k.endsWith(postfix)) | ||||
|                 .map((k) => k.substring(prefix.length, k.length - postfix.length)) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     public GetUnofficialTheme(id: string):  { | ||||
|     public GetUnofficialTheme(id: string): | ||||
|         | { | ||||
|               id: string | ||||
|         icon: string, | ||||
|         title: any, | ||||
|         shortDescription: any, | ||||
|         definition?: any, | ||||
|               icon: string | ||||
|               title: any | ||||
|               shortDescription: any | ||||
|               definition?: any | ||||
|               isOfficial: boolean | ||||
|     } | undefined { | ||||
|           } | ||||
|         | undefined { | ||||
|         console.log("GETTING UNOFFICIAL THEME") | ||||
|         const pref = this.osmConnection.GetLongPreference("unofficial-theme-"+id) | ||||
|         const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) | ||||
|         const str = pref.data | ||||
| 
 | ||||
|         if (str === undefined || str === "undefined" || str === "") { | ||||
|  | @ -172,20 +176,23 @@ export default class UserRelatedState extends ElementsState { | |||
|         try { | ||||
|             const value: { | ||||
|                 id: string | ||||
|                 icon: string, | ||||
|                 title: any, | ||||
|                 shortDescription: any, | ||||
|                 definition?: any, | ||||
|                 icon: string | ||||
|                 title: any | ||||
|                 shortDescription: any | ||||
|                 definition?: any | ||||
|                 isOfficial: boolean | ||||
|             } = JSON.parse(str) | ||||
|             value.isOfficial = false | ||||
|             return value; | ||||
|             return value | ||||
|         } catch (e) { | ||||
|             console.warn("Removing theme " + id + " as it could not be parsed from the preferences; the content is:", str) | ||||
|             console.warn( | ||||
|                 "Removing theme " + | ||||
|                     id + | ||||
|                     " as it could not be parsed from the preferences; the content is:", | ||||
|                 str | ||||
|             ) | ||||
|             pref.setData(null) | ||||
|             return undefined | ||||
|         } | ||||
|          | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,15 +1,14 @@ | |||
| import {TagsFilter} from "./TagsFilter"; | ||||
| import {Or} from "./Or"; | ||||
| import {TagUtils} from "./TagUtils"; | ||||
| import {Tag} from "./Tag"; | ||||
| import {RegexTag} from "./RegexTag"; | ||||
| import { TagsFilter } from "./TagsFilter" | ||||
| import { Or } from "./Or" | ||||
| import { TagUtils } from "./TagUtils" | ||||
| import { Tag } from "./Tag" | ||||
| import { RegexTag } from "./RegexTag" | ||||
| 
 | ||||
| export class And extends TagsFilter { | ||||
| 
 | ||||
|     public and: TagsFilter[] | ||||
| 
 | ||||
|     constructor(and: TagsFilter[]) { | ||||
|         super(); | ||||
|         super() | ||||
|         this.and = and | ||||
|     } | ||||
| 
 | ||||
|  | @ -21,11 +20,11 @@ export class And extends TagsFilter { | |||
|     } | ||||
| 
 | ||||
|     private static combine(filter: string, choices: string[]): string[] { | ||||
|         const values = []; | ||||
|         const values = [] | ||||
|         for (const or of choices) { | ||||
|             values.push(filter + or); | ||||
|             values.push(filter + or) | ||||
|         } | ||||
|         return values; | ||||
|         return values | ||||
|     } | ||||
| 
 | ||||
|     normalize() { | ||||
|  | @ -43,11 +42,11 @@ export class And extends TagsFilter { | |||
|     matchesProperties(tags: any): boolean { | ||||
|         for (const tagsFilter of this.and) { | ||||
|             if (!tagsFilter.matchesProperties(tags)) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -56,36 +55,37 @@ export class And extends TagsFilter { | |||
|      * and.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]" ]
 | ||||
|      */ | ||||
|     asOverpass(): string[] { | ||||
|         let allChoices: string[] = null; | ||||
|         let allChoices: string[] = null | ||||
|         for (const andElement of this.and) { | ||||
|             const andElementFilter = andElement.asOverpass(); | ||||
|             const andElementFilter = andElement.asOverpass() | ||||
|             if (allChoices === null) { | ||||
|                 allChoices = andElementFilter; | ||||
|                 continue; | ||||
|                 allChoices = andElementFilter | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             const newChoices: string[] = []; | ||||
|             const newChoices: string[] = [] | ||||
|             for (const choice of allChoices) { | ||||
|                 newChoices.push( | ||||
|                     ...And.combine(choice, andElementFilter) | ||||
|                 ) | ||||
|                 newChoices.push(...And.combine(choice, andElementFilter)) | ||||
|             } | ||||
|             allChoices = newChoices; | ||||
|             allChoices = newChoices | ||||
|         } | ||||
|         return allChoices; | ||||
|         return allChoices | ||||
|     } | ||||
| 
 | ||||
|     asHumanString(linkToWiki: boolean, shorten: boolean, properties) { | ||||
|         return this.and.map(t => t.asHumanString(linkToWiki, shorten, properties)).filter(x => x !== "").join("&"); | ||||
|         return this.and | ||||
|             .map((t) => t.asHumanString(linkToWiki, shorten, properties)) | ||||
|             .filter((x) => x !== "") | ||||
|             .join("&") | ||||
|     } | ||||
| 
 | ||||
|     isUsableAsAnswer(): boolean { | ||||
|         for (const t of this.and) { | ||||
|             if (!t.isUsableAsAnswer()) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
|         return true; | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -107,45 +107,44 @@ export class And extends TagsFilter { | |||
|      */ | ||||
|     shadows(other: TagsFilter): boolean { | ||||
|         if (!(other instanceof And)) { | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
| 
 | ||||
|         for (const selfTag of this.and) { | ||||
|             let matchFound = false; | ||||
|             let matchFound = false | ||||
|             for (const otherTag of other.and) { | ||||
|                 matchFound = selfTag.shadows(otherTag); | ||||
|                 matchFound = selfTag.shadows(otherTag) | ||||
|                 if (matchFound) { | ||||
|                     break; | ||||
|                     break | ||||
|                 } | ||||
|             } | ||||
|             if (!matchFound) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         for (const otherTag of other.and) { | ||||
|             let matchFound = false; | ||||
|             let matchFound = false | ||||
|             for (const selfTag of this.and) { | ||||
|                 matchFound = selfTag.shadows(otherTag); | ||||
|                 matchFound = selfTag.shadows(otherTag) | ||||
|                 if (matchFound) { | ||||
|                     break; | ||||
|                     break | ||||
|                 } | ||||
|             } | ||||
|             if (!matchFound) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         return true; | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     usedKeys(): string[] { | ||||
|         return [].concat(...this.and.map(subkeys => subkeys.usedKeys())); | ||||
|         return [].concat(...this.and.map((subkeys) => subkeys.usedKeys())) | ||||
|     } | ||||
| 
 | ||||
|     usedTags(): { key: string; value: string }[] { | ||||
|         return [].concat(...this.and.map(subkeys => subkeys.usedTags())); | ||||
|         return [].concat(...this.and.map((subkeys) => subkeys.usedTags())) | ||||
|     } | ||||
| 
 | ||||
|     asChange(properties: any): { k: string; v: string }[] { | ||||
|  | @ -153,7 +152,7 @@ export class And extends TagsFilter { | |||
|         for (const tagsFilter of this.and) { | ||||
|             result.push(...tagsFilter.asChange(properties)) | ||||
|         } | ||||
|         return result; | ||||
|         return result | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -187,7 +186,7 @@ export class And extends TagsFilter { | |||
|                     continue | ||||
|                 } | ||||
|                 if (r === false) { | ||||
|                     return false; | ||||
|                     return false | ||||
|                 } | ||||
|                 newAnds.push(r) | ||||
|                 continue | ||||
|  | @ -203,7 +202,6 @@ export class And extends TagsFilter { | |||
|                 continue | ||||
|             } | ||||
|             if (!value && tag.shadows(knownExpression)) { | ||||
| 
 | ||||
|                 /** | ||||
|                  * We know that knownExpression is unmet. | ||||
|                  * if the tag shadows 'knownExpression' (which is the case when control flows gets here), | ||||
|  | @ -228,13 +226,14 @@ export class And extends TagsFilter { | |||
|         if (this.and.length === 0) { | ||||
|             return true | ||||
|         } | ||||
|         const optimizedRaw = this.and.map(t => t.optimize()) | ||||
|             .filter(t => t !== true /* true is the neutral element in an AND, we drop them*/) | ||||
|         if (optimizedRaw.some(t => t === false)) { | ||||
|         const optimizedRaw = this.and | ||||
|             .map((t) => t.optimize()) | ||||
|             .filter((t) => t !== true /* true is the neutral element in an AND, we drop them*/) | ||||
|         if (optimizedRaw.some((t) => t === false)) { | ||||
|             // We have an AND with a contained false: this is always 'false'
 | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
|         const optimized = <TagsFilter[]>optimizedRaw; | ||||
|         const optimized = <TagsFilter[]>optimizedRaw | ||||
| 
 | ||||
|         { | ||||
|             // Conflicting keys do return false
 | ||||
|  | @ -245,27 +244,27 @@ export class And extends TagsFilter { | |||
|                 } | ||||
|             } | ||||
|             for (const opt of optimized) { | ||||
|                  if(opt instanceof Tag ){ | ||||
|                 if (opt instanceof Tag) { | ||||
|                     const k = opt.key | ||||
|                     const v = properties[k] | ||||
|                      if(v === undefined){ | ||||
|                     if (v === undefined) { | ||||
|                         continue | ||||
|                     } | ||||
|                      if(v !== opt.value){ | ||||
|                     if (v !== opt.value) { | ||||
|                         // detected an internal conflict
 | ||||
|                         return false | ||||
|                     } | ||||
|                 } | ||||
|                  if(opt instanceof RegexTag ){ | ||||
|                 if (opt instanceof RegexTag) { | ||||
|                     const k = opt.key | ||||
|                      if(typeof k !== "string"){ | ||||
|                     if (typeof k !== "string") { | ||||
|                         continue | ||||
|                     } | ||||
|                     const v = properties[k] | ||||
|                      if(v === undefined){ | ||||
|                     if (v === undefined) { | ||||
|                         continue | ||||
|                     } | ||||
|                      if(v !== opt.value){ | ||||
|                     if (v !== opt.value) { | ||||
|                         // detected an internal conflict
 | ||||
|                         return false | ||||
|                     } | ||||
|  | @ -287,7 +286,7 @@ export class And extends TagsFilter { | |||
|         } | ||||
| 
 | ||||
|         { | ||||
|             let dirty = false; | ||||
|             let dirty = false | ||||
|             do { | ||||
|                 const cleanedContainedOrs: Or[] = [] | ||||
|                 outer: for (let containedOr of containedOrs) { | ||||
|  | @ -310,8 +309,8 @@ export class And extends TagsFilter { | |||
|                         } | ||||
|                         // the 'or' dissolved into a normal tag -> it has to be added to the newAnds
 | ||||
|                         newAnds.push(cleaned) | ||||
|                         dirty = true; // rerun this algo later on
 | ||||
|                         continue outer; | ||||
|                         dirty = true // rerun this algo later on
 | ||||
|                         continue outer | ||||
|                     } | ||||
|                     cleanedContainedOrs.push(containedOr) | ||||
|                 } | ||||
|  | @ -319,30 +318,32 @@ export class And extends TagsFilter { | |||
|             } while (dirty) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         containedOrs = containedOrs.filter(ca => { | ||||
|         containedOrs = containedOrs.filter((ca) => { | ||||
|             const isShadowed = TagUtils.containsEquivalents(newAnds, ca.or) | ||||
|             // If 'isShadowed', then at least one part of the 'OR' is matched by the outer and, so this means that this OR isn't needed at all
 | ||||
|             // XY & (XY | AB) === XY
 | ||||
|             return !isShadowed; | ||||
|             return !isShadowed | ||||
|         }) | ||||
| 
 | ||||
|         // Extract common keys from the OR
 | ||||
|         if (containedOrs.length === 1) { | ||||
|             newAnds.push(containedOrs[0]) | ||||
|         } else if (containedOrs.length > 1) { | ||||
|             let commonValues: TagsFilter [] = containedOrs[0].or | ||||
|             let commonValues: TagsFilter[] = containedOrs[0].or | ||||
|             for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++) { | ||||
|                 const containedOr = containedOrs[i]; | ||||
|                 commonValues = commonValues.filter(cv => containedOr.or.some(candidate => candidate.shadows(cv))) | ||||
|                 const containedOr = containedOrs[i] | ||||
|                 commonValues = commonValues.filter((cv) => | ||||
|                     containedOr.or.some((candidate) => candidate.shadows(cv)) | ||||
|                 ) | ||||
|             } | ||||
|             if (commonValues.length === 0) { | ||||
|                 newAnds.push(...containedOrs) | ||||
|             } else { | ||||
|                 const newOrs: TagsFilter[] = [] | ||||
|                 for (const containedOr of containedOrs) { | ||||
|                     const elements = containedOr.or | ||||
|                         .filter(candidate => !commonValues.some(cv => cv.shadows(candidate))) | ||||
|                     const elements = containedOr.or.filter( | ||||
|                         (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) | ||||
|                     ) | ||||
|                     newOrs.push(Or.construct(elements)) | ||||
|                 } | ||||
| 
 | ||||
|  | @ -371,12 +372,11 @@ export class And extends TagsFilter { | |||
|     } | ||||
| 
 | ||||
|     isNegative(): boolean { | ||||
|         return !this.and.some(t => !t.isNegative()); | ||||
|         return !this.and.some((t) => !t.isNegative()) | ||||
|     } | ||||
| 
 | ||||
|     visit(f: (TagsFilter: any) => void) { | ||||
|         f(this) | ||||
|         this.and.forEach(sub => sub.visit(f)) | ||||
|         this.and.forEach((sub) => sub.visit(f)) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,14 +1,18 @@ | |||
| import {TagsFilter} from "./TagsFilter"; | ||||
| import { TagsFilter } from "./TagsFilter" | ||||
| 
 | ||||
| export default class ComparingTag implements TagsFilter { | ||||
|     private readonly _key: string; | ||||
|     private readonly _predicate: (value: string) => boolean; | ||||
|     private readonly _representation: string; | ||||
|     private readonly _key: string | ||||
|     private readonly _predicate: (value: string) => boolean | ||||
|     private readonly _representation: string | ||||
| 
 | ||||
|     constructor(key: string, predicate: (value: string | undefined) => boolean, representation: string = "") { | ||||
|         this._key = key; | ||||
|         this._predicate = predicate; | ||||
|         this._representation = representation; | ||||
|     constructor( | ||||
|         key: string, | ||||
|         predicate: (value: string | undefined) => boolean, | ||||
|         representation: string = "" | ||||
|     ) { | ||||
|         this._key = key | ||||
|         this._predicate = predicate | ||||
|         this._representation = representation | ||||
|     } | ||||
| 
 | ||||
|     asChange(properties: any): { k: string; v: string }[] { | ||||
|  | @ -24,11 +28,11 @@ export default class ComparingTag implements TagsFilter { | |||
|     } | ||||
| 
 | ||||
|     shadows(other: TagsFilter): boolean { | ||||
|         return other === this; | ||||
|         return other === this | ||||
|     } | ||||
| 
 | ||||
|     isUsableAsAnswer(): boolean { | ||||
|         return false; | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -41,23 +45,23 @@ export default class ComparingTag implements TagsFilter { | |||
|      * t.matchesProperties({differentKey: 42}) // => false
 | ||||
|      */ | ||||
|     matchesProperties(properties: any): boolean { | ||||
|         return this._predicate(properties[this._key]); | ||||
|         return this._predicate(properties[this._key]) | ||||
|     } | ||||
| 
 | ||||
|     usedKeys(): string[] { | ||||
|         return [this._key]; | ||||
|         return [this._key] | ||||
|     } | ||||
| 
 | ||||
|     usedTags(): { key: string; value: string }[] { | ||||
|         return []; | ||||
|         return [] | ||||
|     } | ||||
| 
 | ||||
|     optimize(): TagsFilter | boolean { | ||||
|         return this; | ||||
|         return this | ||||
|     } | ||||
| 
 | ||||
|     isNegative(): boolean { | ||||
|         return true; | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     visit(f: (TagsFilter) => void) { | ||||
|  |  | |||
							
								
								
									
										142
									
								
								Logic/Tags/Or.ts
									
										
									
									
									
								
							
							
						
						
									
										142
									
								
								Logic/Tags/Or.ts
									
										
									
									
									
								
							|  | @ -1,32 +1,30 @@ | |||
| import {TagsFilter} from "./TagsFilter"; | ||||
| import {TagUtils} from "./TagUtils"; | ||||
| import {And} from "./And"; | ||||
| 
 | ||||
| import { TagsFilter } from "./TagsFilter" | ||||
| import { TagUtils } from "./TagUtils" | ||||
| import { And } from "./And" | ||||
| 
 | ||||
| export class Or extends TagsFilter { | ||||
|     public or: TagsFilter[] | ||||
| 
 | ||||
|     constructor(or: TagsFilter[]) { | ||||
|         super(); | ||||
|         this.or = or; | ||||
|         super() | ||||
|         this.or = or | ||||
|     } | ||||
| 
 | ||||
|     public static construct(or: TagsFilter[]): TagsFilter{ | ||||
|         if(or.length === 1){ | ||||
|     public static construct(or: TagsFilter[]): TagsFilter { | ||||
|         if (or.length === 1) { | ||||
|             return or[0] | ||||
|         } | ||||
|         return new Or(or) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     matchesProperties(properties: any): boolean { | ||||
|         for (const tagsFilter of this.or) { | ||||
|             if (tagsFilter.matchesProperties(properties)) { | ||||
|                 return true; | ||||
|                 return true | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -43,46 +41,45 @@ export class Or extends TagsFilter { | |||
|      * or.asOverpass() // => [ `["key"="value"]`, `["key1"="value1"]`, `["key2"="value2"]` ]
 | ||||
|      */ | ||||
|     asOverpass(): string[] { | ||||
|         const choices = []; | ||||
|         const choices = [] | ||||
|         for (const tagsFilter of this.or) { | ||||
|             const subChoices = tagsFilter.asOverpass(); | ||||
|             const subChoices = tagsFilter.asOverpass() | ||||
|             choices.push(...subChoices) | ||||
|         } | ||||
|         return choices; | ||||
|         return choices | ||||
|     } | ||||
| 
 | ||||
|     asHumanString(linkToWiki: boolean, shorten: boolean, properties) { | ||||
|         return this.or.map(t => t.asHumanString(linkToWiki, shorten, properties)).join("|"); | ||||
|         return this.or.map((t) => t.asHumanString(linkToWiki, shorten, properties)).join("|") | ||||
|     } | ||||
| 
 | ||||
|     isUsableAsAnswer(): boolean { | ||||
|         return false; | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     shadows(other: TagsFilter): boolean { | ||||
|         if (other instanceof Or) { | ||||
| 
 | ||||
|             for (const selfTag of this.or) { | ||||
|                 let matchFound = false; | ||||
|                 let matchFound = false | ||||
|                 for (let i = 0; i < other.or.length && !matchFound; i++) { | ||||
|                     let otherTag = other.or[i]; | ||||
|                     matchFound = selfTag.shadows(otherTag); | ||||
|                     let otherTag = other.or[i] | ||||
|                     matchFound = selfTag.shadows(otherTag) | ||||
|                 } | ||||
|                 if (!matchFound) { | ||||
|                     return false; | ||||
|                     return false | ||||
|                 } | ||||
|             } | ||||
|             return true; | ||||
|             return true | ||||
|         } | ||||
|         return false; | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     usedKeys(): string[] { | ||||
|         return [].concat(...this.or.map(subkeys => subkeys.usedKeys())); | ||||
|         return [].concat(...this.or.map((subkeys) => subkeys.usedKeys())) | ||||
|     } | ||||
| 
 | ||||
|     usedTags(): { key: string; value: string }[] { | ||||
|         return [].concat(...this.or.map(subkeys => subkeys.usedTags())); | ||||
|         return [].concat(...this.or.map((subkeys) => subkeys.usedTags())) | ||||
|     } | ||||
| 
 | ||||
|     asChange(properties: any): { k: string; v: string }[] { | ||||
|  | @ -90,7 +87,7 @@ export class Or extends TagsFilter { | |||
|         for (const tagsFilter of this.or) { | ||||
|             result.push(...tagsFilter.asChange(properties)) | ||||
|         } | ||||
|         return result; | ||||
|         return result | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -109,21 +106,21 @@ export class Or extends TagsFilter { | |||
|     removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { | ||||
|         const newOrs: TagsFilter[] = [] | ||||
|         for (const tag of this.or) { | ||||
|             if(tag instanceof Or){ | ||||
|             if (tag instanceof Or) { | ||||
|                 throw "Optimize expressions before using removePhraseConsideredKnown" | ||||
|             } | ||||
|             if(tag instanceof And){ | ||||
|             if (tag instanceof And) { | ||||
|                 const r = tag.removePhraseConsideredKnown(knownExpression, value) | ||||
|                 if(r === false){ | ||||
|                 if (r === false) { | ||||
|                     continue | ||||
|                 } | ||||
|                 if(r === true){ | ||||
|                     return true; | ||||
|                 if (r === true) { | ||||
|                     return true | ||||
|                 } | ||||
|                 newOrs.push(r) | ||||
|                 continue | ||||
|             } | ||||
|             if(value && knownExpression.shadows(tag)){ | ||||
|             if (value && knownExpression.shadows(tag)) { | ||||
|                 /** | ||||
|                  * At this point, we do know that 'knownExpression' is true in every case | ||||
|                  * As `shadows` does define that 'tag' MUST be true if 'knownExpression' is true, | ||||
|  | @ -131,10 +128,9 @@ export class Or extends TagsFilter { | |||
|                  * | ||||
|                  * "True" is the absorbing element in an OR, so we can return true | ||||
|                  */ | ||||
|                 return true; | ||||
|                 return true | ||||
|             } | ||||
|             if(!value && tag.shadows(knownExpression)){ | ||||
| 
 | ||||
|             if (!value && tag.shadows(knownExpression)) { | ||||
|                 /** | ||||
|                  * We know that knownExpression is unmet. | ||||
|                  * if the tag shadows 'knownExpression' (which is the case when control flows gets here), | ||||
|  | @ -147,34 +143,33 @@ export class Or extends TagsFilter { | |||
|             } | ||||
|             newOrs.push(tag) | ||||
|         } | ||||
|         if(newOrs.length === 0){ | ||||
|         if (newOrs.length === 0) { | ||||
|             return false | ||||
|         } | ||||
|         return Or.construct(newOrs) | ||||
|     } | ||||
| 
 | ||||
|     optimize(): TagsFilter | boolean { | ||||
|          | ||||
|         if(this.or.length === 0){ | ||||
|             return false; | ||||
|         if (this.or.length === 0) { | ||||
|             return false | ||||
|         } | ||||
| 
 | ||||
|         const optimizedRaw = this.or.map(t => t.optimize()) | ||||
|             .filter(t => t !== false /* false is the neutral element in an OR, we drop them*/ ) | ||||
|         if(optimizedRaw.some(t => t === true)){ | ||||
|         const optimizedRaw = this.or | ||||
|             .map((t) => t.optimize()) | ||||
|             .filter((t) => t !== false /* false is the neutral element in an OR, we drop them*/) | ||||
|         if (optimizedRaw.some((t) => t === true)) { | ||||
|             // We have an OR with a contained true: this is always 'true'
 | ||||
|             return true; | ||||
|             return true | ||||
|         } | ||||
|         const optimized = <TagsFilter[]> optimizedRaw; | ||||
|         const optimized = <TagsFilter[]>optimizedRaw | ||||
| 
 | ||||
| 
 | ||||
|         const newOrs : TagsFilter[] = [] | ||||
|         let containedAnds : And[] = [] | ||||
|         const newOrs: TagsFilter[] = [] | ||||
|         let containedAnds: And[] = [] | ||||
|         for (const tf of optimized) { | ||||
|             if(tf instanceof Or){ | ||||
|             if (tf instanceof Or) { | ||||
|                 // expand all the nested ors...
 | ||||
|                 newOrs.push(...tf.or) | ||||
|             }else if(tf instanceof And){ | ||||
|             } else if (tf instanceof And) { | ||||
|                 // partition of all the ands
 | ||||
|                 containedAnds.push(tf) | ||||
|             } else { | ||||
|  | @ -183,9 +178,9 @@ export class Or extends TagsFilter { | |||
|         } | ||||
| 
 | ||||
|         { | ||||
|             let dirty = false; | ||||
|             let dirty = false | ||||
|             do { | ||||
|                 const cleanedContainedANds : And[] = [] | ||||
|                 const cleanedContainedANds: And[] = [] | ||||
|                 outer: for (let containedAnd of containedAnds) { | ||||
|                     for (const known of newOrs) { | ||||
|                         // input for optimazation: (K=V | (X=Y & K=V))
 | ||||
|  | @ -206,49 +201,53 @@ export class Or extends TagsFilter { | |||
|                         } | ||||
|                         // the 'and' dissolved into a normal tag -> it has to be added to the newOrs
 | ||||
|                         newOrs.push(cleaned) | ||||
|                         dirty = true; // rerun this algo later on
 | ||||
|                         continue outer; | ||||
|                         dirty = true // rerun this algo later on
 | ||||
|                         continue outer | ||||
|                     } | ||||
|                     cleanedContainedANds.push(containedAnd) | ||||
|                 } | ||||
|                 containedAnds = cleanedContainedANds | ||||
|             } while(dirty) | ||||
|             } while (dirty) | ||||
|         } | ||||
|         // Extract common keys from the ANDS
 | ||||
|         if(containedAnds.length === 1){ | ||||
|         if (containedAnds.length === 1) { | ||||
|             newOrs.push(containedAnds[0]) | ||||
|         } else if(containedAnds.length > 1){ | ||||
|             let commonValues : TagsFilter [] = containedAnds[0].and | ||||
|             for (let i = 1; i < containedAnds.length && commonValues.length > 0; i++){ | ||||
|                 const containedAnd = containedAnds[i]; | ||||
|                 commonValues = commonValues.filter(cv => containedAnd.and.some(candidate => candidate.shadows(cv))) | ||||
|         } else if (containedAnds.length > 1) { | ||||
|             let commonValues: TagsFilter[] = containedAnds[0].and | ||||
|             for (let i = 1; i < containedAnds.length && commonValues.length > 0; i++) { | ||||
|                 const containedAnd = containedAnds[i] | ||||
|                 commonValues = commonValues.filter((cv) => | ||||
|                     containedAnd.and.some((candidate) => candidate.shadows(cv)) | ||||
|                 ) | ||||
|             } | ||||
|             if(commonValues.length === 0){ | ||||
|             if (commonValues.length === 0) { | ||||
|                 newOrs.push(...containedAnds) | ||||
|             }else{ | ||||
|             } else { | ||||
|                 const newAnds: TagsFilter[] = [] | ||||
|                 for (const containedAnd of containedAnds) { | ||||
|                     const elements = containedAnd.and.filter(candidate => !commonValues.some(cv => cv.shadows(candidate))) | ||||
|                     const elements = containedAnd.and.filter( | ||||
|                         (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) | ||||
|                     ) | ||||
|                     newAnds.push(And.construct(elements)) | ||||
|                 } | ||||
| 
 | ||||
|                 commonValues.push(Or.construct(newAnds)) | ||||
|                 const result = new And(commonValues).optimize() | ||||
|                 if(result === true){ | ||||
|                 if (result === true) { | ||||
|                     return true | ||||
|                 }else if(result === false){ | ||||
|                 } else if (result === false) { | ||||
|                     // neutral element: skip
 | ||||
|                 }else{ | ||||
|                 } else { | ||||
|                     newOrs.push(And.construct(commonValues)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if(newOrs.length === 0){ | ||||
|         if (newOrs.length === 0) { | ||||
|             return false | ||||
|         } | ||||
| 
 | ||||
|         if(TagUtils.ContainsOppositeTags(newOrs)){ | ||||
|         if (TagUtils.ContainsOppositeTags(newOrs)) { | ||||
|             return true | ||||
|         } | ||||
| 
 | ||||
|  | @ -258,14 +257,11 @@ export class Or extends TagsFilter { | |||
|     } | ||||
| 
 | ||||
|     isNegative(): boolean { | ||||
|         return this.or.some(t => t.isNegative()); | ||||
|         return this.or.some((t) => t.isNegative()) | ||||
|     } | ||||
| 
 | ||||
|     visit(f: (TagsFilter: any) => void) { | ||||
|         f(this) | ||||
|         this.or.forEach(t => t.visit(f)) | ||||
|         this.or.forEach((t) => t.visit(f)) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,38 +1,38 @@ | |||
| import {Tag} from "./Tag"; | ||||
| import {TagsFilter} from "./TagsFilter"; | ||||
| import { Tag } from "./Tag" | ||||
| import { TagsFilter } from "./TagsFilter" | ||||
| 
 | ||||
| export class RegexTag extends TagsFilter { | ||||
|     public readonly key: RegExp | string; | ||||
|     public readonly value: RegExp | string; | ||||
|     public readonly invert: boolean; | ||||
|     public readonly key: RegExp | string | ||||
|     public readonly value: RegExp | string | ||||
|     public readonly invert: boolean | ||||
|     public readonly matchesEmpty: boolean | ||||
| 
 | ||||
|     constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) { | ||||
|         super(); | ||||
|         this.key = key; | ||||
|         this.value = value; | ||||
|         this.invert = invert; | ||||
|         this.matchesEmpty = RegexTag.doesMatch("", this.value); | ||||
|         super() | ||||
|         this.key = key | ||||
|         this.value = value | ||||
|         this.invert = invert | ||||
|         this.matchesEmpty = RegexTag.doesMatch("", this.value) | ||||
|     } | ||||
| 
 | ||||
|     private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean { | ||||
|         if (fromTag === undefined) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         if (typeof fromTag === "number") { | ||||
|             fromTag = "" + fromTag; | ||||
|             fromTag = "" + fromTag | ||||
|         } | ||||
|         if (typeof possibleRegex === "string") { | ||||
|             return fromTag === possibleRegex; | ||||
|             return fromTag === possibleRegex | ||||
|         } | ||||
|         return fromTag.match(possibleRegex) !== null; | ||||
|         return fromTag.match(possibleRegex) !== null | ||||
|     } | ||||
| 
 | ||||
|     private static source(r: string | RegExp) { | ||||
|         if (typeof (r) === "string") { | ||||
|             return r; | ||||
|         if (typeof r === "string") { | ||||
|             return r | ||||
|         } | ||||
|         return r.source; | ||||
|         return r.source | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -48,29 +48,28 @@ export class RegexTag extends TagsFilter { | |||
|      * new RegexTag("key", /^.*value.*$/i).asOverpass() // => [ `["key"~\"^.*value.*$\",i]` ]
 | ||||
|      */ | ||||
|     asOverpass(): string[] { | ||||
|         const inv =this.invert ? "!" : "" | ||||
|         const inv = this.invert ? "!" : "" | ||||
|         if (typeof this.key !== "string") { | ||||
|             // The key is a regex too
 | ||||
|             return [`[~"${this.key.source}"${inv}~"${RegexTag.source(this.value)}"]`]; | ||||
|             return [`[~"${this.key.source}"${inv}~"${RegexTag.source(this.value)}"]`] | ||||
|         } | ||||
| 
 | ||||
|         if(this.value instanceof RegExp){ | ||||
|             const src =this.value.source | ||||
|             if(src === "^..*$"){ | ||||
|         if (this.value instanceof RegExp) { | ||||
|             const src = this.value.source | ||||
|             if (src === "^..*$") { | ||||
|                 // anything goes
 | ||||
|                 return [`[${inv}"${this.key}"]`] | ||||
|             } | ||||
|             const modifier = this.value.ignoreCase ? ",i" : "" | ||||
|             return [`["${this.key}"${inv}~"${src}"${modifier}]`] | ||||
|         }else{ | ||||
|         } else { | ||||
|             // Normal key and normal value
 | ||||
|             return [`["${this.key}"${inv}="${this.value}"]`]; | ||||
|             return [`["${this.key}"${inv}="${this.value}"]`] | ||||
|         } | ||||
|          | ||||
|     } | ||||
| 
 | ||||
|     isUsableAsAnswer(): boolean { | ||||
|         return false; | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -125,30 +124,30 @@ export class RegexTag extends TagsFilter { | |||
|     matchesProperties(tags: any): boolean { | ||||
|         if (typeof this.key === "string") { | ||||
|             const value = tags[this.key] ?? "" | ||||
|             return RegexTag.doesMatch(value, this.value) != this.invert; | ||||
|             return RegexTag.doesMatch(value, this.value) != this.invert | ||||
|         } | ||||
| 
 | ||||
|         for (const key in tags) { | ||||
|             if (key === undefined) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
|             if (RegexTag.doesMatch(key, this.key)) { | ||||
|                 const value = tags[key] ?? ""; | ||||
|                 return RegexTag.doesMatch(value, this.value) != this.invert; | ||||
|                 const value = tags[key] ?? "" | ||||
|                 return RegexTag.doesMatch(value, this.value) != this.invert | ||||
|             } | ||||
|         } | ||||
|         if (this.matchesEmpty) { | ||||
|             // The value is 'empty'
 | ||||
|             return !this.invert; | ||||
|             return !this.invert | ||||
|         } | ||||
|         // The matching key was not found
 | ||||
|         return this.invert; | ||||
|         return this.invert | ||||
|     } | ||||
| 
 | ||||
|     asHumanString() { | ||||
|         if (typeof this.key === "string") { | ||||
|             const oper = typeof this.value === "string" ? "=" : "~" | ||||
|             return `${this.key}${this.invert ? "!" : ""}${oper}${RegexTag.source(this.value)}`; | ||||
|             return `${this.key}${this.invert ? "!" : ""}${oper}${RegexTag.source(this.value)}` | ||||
|         } | ||||
|         return `${this.key.source}${this.invert ? "!" : ""}~~${RegexTag.source(this.value)}` | ||||
|     } | ||||
|  | @ -173,45 +172,46 @@ export class RegexTag extends TagsFilter { | |||
|      */ | ||||
|     shadows(other: TagsFilter): boolean { | ||||
|         if (other instanceof RegexTag) { | ||||
|             if((other.key["source"] ?? other.key) !== (this.key["source"] ?? this.key) ){ | ||||
|             if ((other.key["source"] ?? other.key) !== (this.key["source"] ?? this.key)) { | ||||
|                 // Keys don't match, never shadowing
 | ||||
|                 return false | ||||
|             } | ||||
|             if((other.value["source"] ?? other.key) === (this.value["source"] ?? this.key) && this.invert  == other.invert ){ | ||||
|             if ( | ||||
|                 (other.value["source"] ?? other.key) === (this.value["source"] ?? this.key) && | ||||
|                 this.invert == other.invert | ||||
|             ) { | ||||
|                 // Values (and inverts) match
 | ||||
|                 return true | ||||
|             } | ||||
|             if(typeof other.value ==="string"){ | ||||
|             if (typeof other.value === "string") { | ||||
|                 const valuesMatch = RegexTag.doesMatch(other.value, this.value) | ||||
|                 if(!this.invert && !other.invert){ | ||||
|                 if (!this.invert && !other.invert) { | ||||
|                     // this: key~value, other: key=value
 | ||||
|                     return valuesMatch | ||||
|                 } | ||||
|                 if(this.invert && !other.invert){ | ||||
|                 if (this.invert && !other.invert) { | ||||
|                     // this: key!~value, other: key=value
 | ||||
|                     return !valuesMatch | ||||
|                 } | ||||
|                 if(!this.invert && other.invert){ | ||||
|                 if (!this.invert && other.invert) { | ||||
|                     // this: key~value, other: key!=value
 | ||||
|                     return !valuesMatch | ||||
|                 } | ||||
|                 if(!this.invert && !other.invert){ | ||||
|                 if (!this.invert && !other.invert) { | ||||
|                     // this: key!~value, other: key!=value
 | ||||
|                     return valuesMatch | ||||
|                 } | ||||
|                  | ||||
|             } | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
|         if (other instanceof Tag) { | ||||
|             if(!RegexTag.doesMatch(other.key, this.key)){ | ||||
|             if (!RegexTag.doesMatch(other.key, this.key)) { | ||||
|                 // Keys don't match
 | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
| 
 | ||||
|              | ||||
|             if(this.value["source"] === "^..*$") { | ||||
|                 if(this.invert){ | ||||
|             if (this.value["source"] === "^..*$") { | ||||
|                 if (this.invert) { | ||||
|                     return other.value === "" | ||||
|                 } | ||||
|                 return false | ||||
|  | @ -224,23 +224,23 @@ export class RegexTag extends TagsFilter { | |||
|                  * actual property: a=x | ||||
|                  * In other words: shadowing will never occur here | ||||
|                  */ | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
|             // Unless the values are the same, it is pretty hard to figure out if they are shadowing. This is future work
 | ||||
|             return (this.value["source"] ?? this.value) === other.value; | ||||
|             return (this.value["source"] ?? this.value) === other.value | ||||
|         } | ||||
|         return false; | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     usedKeys(): string[] { | ||||
|         if (typeof this.key === "string") { | ||||
|             return [this.key]; | ||||
|             return [this.key] | ||||
|         } | ||||
|         throw "Key cannot be determined as it is a regex" | ||||
|     } | ||||
| 
 | ||||
|     usedTags(): { key: string; value: string }[] { | ||||
|         return []; | ||||
|         return [] | ||||
|     } | ||||
| 
 | ||||
|     asChange(properties: any): { k: string; v: string }[] { | ||||
|  | @ -249,23 +249,23 @@ export class RegexTag extends TagsFilter { | |||
|         } | ||||
|         if (typeof this.key === "string") { | ||||
|             if (typeof this.value === "string") { | ||||
|                 return [{k: this.key, v: this.value}] | ||||
|                 return [{ k: this.key, v: this.value }] | ||||
|             } | ||||
|             if (this.value.toString() != "/^..*$/") { | ||||
|                 console.warn("Regex value in tag; using wildcard:", this.key, this.value) | ||||
|             } | ||||
|             return [{k: this.key, v: undefined}] | ||||
|             return [{ k: this.key, v: undefined }] | ||||
|         } | ||||
|         console.error("Cannot export regex tag to asChange; ", this.key, this.value) | ||||
|         return [] | ||||
|     } | ||||
| 
 | ||||
|     optimize(): TagsFilter | boolean { | ||||
|         return this; | ||||
|         return this | ||||
|     } | ||||
| 
 | ||||
|     isNegative(): boolean { | ||||
|         return this.invert; | ||||
|         return this.invert | ||||
|     } | ||||
| 
 | ||||
|     visit(f: (TagsFilter) => void) { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {TagsFilter} from "./TagsFilter"; | ||||
| import {Tag} from "./Tag"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import { TagsFilter } from "./TagsFilter" | ||||
| import { Tag } from "./Tag" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| /** | ||||
|  * The substituting-tag uses the tags of a feature a variables and replaces them. | ||||
|  | @ -12,32 +12,37 @@ import {Utils} from "../../Utils"; | |||
|  * This cannot be used to query features | ||||
|  */ | ||||
| export default class SubstitutingTag implements TagsFilter { | ||||
|     private readonly _key: string; | ||||
|     private readonly _value: string; | ||||
|     private readonly _key: string | ||||
|     private readonly _value: string | ||||
|     private readonly _invert: boolean | ||||
| 
 | ||||
|     constructor(key: string, value: string, invert = false) { | ||||
|         this._key = key; | ||||
|         this._value = value; | ||||
|         this._key = key | ||||
|         this._value = value | ||||
|         this._invert = invert | ||||
|     } | ||||
| 
 | ||||
|     private static substituteString(template: string, dict: any): string { | ||||
|         for (const k in dict) { | ||||
|             template = template.replace(new RegExp("\\{" + k + "\\}", 'g'), dict[k]) | ||||
|             template = template.replace(new RegExp("\\{" + k + "\\}", "g"), dict[k]) | ||||
|         } | ||||
|         return template.replace(/{.*}/g, ""); | ||||
|         return template.replace(/{.*}/g, "") | ||||
|     } | ||||
| 
 | ||||
|     asTag(currentProperties: Record<string, string>){ | ||||
|         if(this._invert){ | ||||
|     asTag(currentProperties: Record<string, string>) { | ||||
|         if (this._invert) { | ||||
|             throw "Cannot convert an inverted substituting tag" | ||||
|         } | ||||
|         return new Tag(this._key, Utils.SubstituteKeys(this._value, currentProperties)) | ||||
|     } | ||||
| 
 | ||||
|     asHumanString(linkToWiki: boolean, shorten: boolean, properties) { | ||||
|         return this._key + (this._invert ? '!' : '') + "=" + SubstitutingTag.substituteString(this._value, properties); | ||||
|         return ( | ||||
|             this._key + | ||||
|             (this._invert ? "!" : "") + | ||||
|             "=" + | ||||
|             SubstitutingTag.substituteString(this._value, properties) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     asOverpass(): string[] { | ||||
|  | @ -46,13 +51,17 @@ export default class SubstitutingTag implements TagsFilter { | |||
| 
 | ||||
|     shadows(other: TagsFilter): boolean { | ||||
|         if (!(other instanceof SubstitutingTag)) { | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
|         return other._key === this._key && other._value === this._value && other._invert === this._invert; | ||||
|         return ( | ||||
|             other._key === this._key && | ||||
|             other._value === this._value && | ||||
|             other._invert === this._invert | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     isUsableAsAnswer(): boolean { | ||||
|         return !this._invert; | ||||
|         return !this._invert | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -64,16 +73,16 @@ export default class SubstitutingTag implements TagsFilter { | |||
|      * assign.matchesProperties({"some_key": "2021-03-29"}) // => false
 | ||||
|      */ | ||||
|     matchesProperties(properties: any): boolean { | ||||
|         const value = properties[this._key]; | ||||
|         const value = properties[this._key] | ||||
|         if (value === undefined || value === "") { | ||||
|             return false; | ||||
|             return false | ||||
|         } | ||||
|         const expectedValue = SubstitutingTag.substituteString(this._value, properties); | ||||
|         return (value === expectedValue) !== this._invert; | ||||
|         const expectedValue = SubstitutingTag.substituteString(this._value, properties) | ||||
|         return (value === expectedValue) !== this._invert | ||||
|     } | ||||
| 
 | ||||
|     usedKeys(): string[] { | ||||
|         return [this._key]; | ||||
|         return [this._key] | ||||
|     } | ||||
| 
 | ||||
|     usedTags(): { key: string; value: string }[] { | ||||
|  | @ -84,19 +93,19 @@ export default class SubstitutingTag implements TagsFilter { | |||
|         if (this._invert) { | ||||
|             throw "An inverted substituting tag can not be used to create a change" | ||||
|         } | ||||
|         const v = SubstitutingTag.substituteString(this._value, properties); | ||||
|         const v = SubstitutingTag.substituteString(this._value, properties) | ||||
|         if (v.match(/{.*}/) !== null) { | ||||
|             throw "Could not calculate all the substitutions: still have " + v | ||||
|         } | ||||
|         return [{k: this._key, v: v}]; | ||||
|         return [{ k: this._key, v: v }] | ||||
|     } | ||||
| 
 | ||||
|     optimize(): TagsFilter | boolean { | ||||
|         return this; | ||||
|         return this | ||||
|     } | ||||
| 
 | ||||
|     isNegative(): boolean { | ||||
|         return false; | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     visit(f: (TagsFilter: any) => void) { | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| import {Utils} from "../../Utils"; | ||||
| import {TagsFilter} from "./TagsFilter"; | ||||
| 
 | ||||
| import { Utils } from "../../Utils" | ||||
| import { TagsFilter } from "./TagsFilter" | ||||
| 
 | ||||
| export class Tag extends TagsFilter { | ||||
|     public key: string | ||||
|  | @ -10,21 +9,22 @@ export class Tag extends TagsFilter { | |||
|         this.key = key | ||||
|         this.value = value | ||||
|         if (key === undefined || key === "") { | ||||
|             throw "Invalid key: undefined or empty"; | ||||
|             throw "Invalid key: undefined or empty" | ||||
|         } | ||||
|         if (value === undefined) { | ||||
|             throw `Invalid value while constructing a Tag with key '${key}': value is undefined`; | ||||
|             throw `Invalid value while constructing a Tag with key '${key}': value is undefined` | ||||
|         } | ||||
|         if (value === "*") { | ||||
|             console.warn(`Got suspicious tag ${key}=*   ; did you mean ${key}~* ?`) | ||||
|         } | ||||
|         if(value.indexOf("&") >= 0){ | ||||
|             const tags = (key + "="+value).split("&") | ||||
|             throw `Invalid value for a tag: it contains '&'. You probably meant to use '{"and":[${tags.map(kv => "\"" + kv +"\"").join(', ')}]}'` | ||||
|         if (value.indexOf("&") >= 0) { | ||||
|             const tags = (key + "=" + value).split("&") | ||||
|             throw `Invalid value for a tag: it contains '&'. You probably meant to use '{"and":[${tags | ||||
|                 .map((kv) => '"' + kv + '"') | ||||
|                 .join(", ")}]}'` | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * imort | ||||
|      * | ||||
|  | @ -48,18 +48,18 @@ export class Tag extends TagsFilter { | |||
|         if (foundValue === undefined && (this.value === "" || this.value === undefined)) { | ||||
|             // The tag was not found
 | ||||
|             // and it shouldn't be found!
 | ||||
|             return true; | ||||
|             return true | ||||
|         } | ||||
| 
 | ||||
|         return foundValue === this.value; | ||||
|         return foundValue === this.value | ||||
|     } | ||||
| 
 | ||||
|     asOverpass(): string[] { | ||||
|         if (this.value === "") { | ||||
|             // NOT having this key
 | ||||
|             return ['[!"' + this.key + '"]']; | ||||
|             return ['[!"' + this.key + '"]'] | ||||
|         } | ||||
|         return [`["${this.key}"="${this.value}"]`]; | ||||
|         return [`["${this.key}"="${this.value}"]`] | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -69,11 +69,11 @@ export class Tag extends TagsFilter { | |||
|      t.asHumanString(true) // => "<a href='https://wiki.openstreetmap.org/wiki/Key:key' target='_blank'>key</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:key%3Dvalue' target='_blank'>value</a>"
 | ||||
|      */ | ||||
|     asHumanString(linkToWiki?: boolean, shorten?: boolean, currentProperties?: any) { | ||||
|         let v = this.value; | ||||
|         let v = this.value | ||||
|         if (shorten) { | ||||
|             v = Utils.EllipsesAfter(v, 25); | ||||
|             v = Utils.EllipsesAfter(v, 25) | ||||
|         } | ||||
|         if (v === "" || v === undefined && currentProperties !== undefined) { | ||||
|         if (v === "" || (v === undefined && currentProperties !== undefined)) { | ||||
|             // This tag will be removed if in the properties, so we indicate this with special rendering
 | ||||
|             if (currentProperties !== undefined && (currentProperties[this.key] ?? "") === "") { | ||||
|                 // This tag is not present in the current properties, so this tag doesn't change anything
 | ||||
|  | @ -82,15 +82,17 @@ export class Tag extends TagsFilter { | |||
|             return "<span class='line-through'>" + this.key + "</span>" | ||||
|         } | ||||
|         if (linkToWiki) { | ||||
|             return `<a href='https://wiki.openstreetmap.org/wiki/Key:${this.key}' target='_blank'>${this.key}</a>` + | ||||
|             return ( | ||||
|                 `<a href='https://wiki.openstreetmap.org/wiki/Key:${this.key}' target='_blank'>${this.key}</a>` + | ||||
|                 `=` + | ||||
|                 `<a href='https://wiki.openstreetmap.org/wiki/Tag:${this.key}%3D${this.value}' target='_blank'>${v}</a>` | ||||
|             ) | ||||
|         } | ||||
|         return this.key + "=" + v; | ||||
|         return this.key + "=" + v | ||||
|     } | ||||
| 
 | ||||
|     isUsableAsAnswer(): boolean { | ||||
|         return true; | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -107,35 +109,35 @@ export class Tag extends TagsFilter { | |||
|      * new Tag("key","value").shadows(new RegexTag("otherkey", "value", false)) // => false
 | ||||
|      */ | ||||
|     shadows(other: TagsFilter): boolean { | ||||
|         if(other["key"] !== undefined){ | ||||
|             if(other["key"] !== this.key){ | ||||
|         if (other["key"] !== undefined) { | ||||
|             if (other["key"] !== this.key) { | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
|         return other.matchesProperties({[this.key]: this.value}); | ||||
|         return other.matchesProperties({ [this.key]: this.value }) | ||||
|     } | ||||
| 
 | ||||
|     usedKeys(): string[] { | ||||
|         return [this.key]; | ||||
|         return [this.key] | ||||
|     } | ||||
| 
 | ||||
|     usedTags(): { key: string; value: string }[] { | ||||
|         if(this.value == ""){ | ||||
|         if (this.value == "") { | ||||
|             return [] | ||||
|         } | ||||
|         return [this] | ||||
|     } | ||||
| 
 | ||||
|     asChange(properties: any): { k: string; v: string }[] { | ||||
|         return [{k: this.key, v: this.value}]; | ||||
|         return [{ k: this.key, v: this.value }] | ||||
|     } | ||||
| 
 | ||||
|     optimize(): TagsFilter | boolean { | ||||
|         return this; | ||||
|         return this | ||||
|     } | ||||
| 
 | ||||
|     isNegative(): boolean { | ||||
|         return false; | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     visit(f: (TagsFilter) => void) { | ||||
|  |  | |||
|  | @ -1,23 +1,21 @@ | |||
| import {Tag} from "./Tag"; | ||||
| import {TagsFilter} from "./TagsFilter"; | ||||
| import {And} from "./And"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import ComparingTag from "./ComparingTag"; | ||||
| import {RegexTag} from "./RegexTag"; | ||||
| import SubstitutingTag from "./SubstitutingTag"; | ||||
| import {Or} from "./Or"; | ||||
| import {TagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson"; | ||||
| import {isRegExp} from "util"; | ||||
| import { Tag } from "./Tag" | ||||
| import { TagsFilter } from "./TagsFilter" | ||||
| import { And } from "./And" | ||||
| import { Utils } from "../../Utils" | ||||
| import ComparingTag from "./ComparingTag" | ||||
| import { RegexTag } from "./RegexTag" | ||||
| import SubstitutingTag from "./SubstitutingTag" | ||||
| import { Or } from "./Or" | ||||
| import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||
| import { isRegExp } from "util" | ||||
| import * as key_counts from "../../assets/key_totals.json" | ||||
| 
 | ||||
| type Tags = Record<string, string> | ||||
| export type UploadableTag = Tag | SubstitutingTag | And | ||||
| 
 | ||||
| export class TagUtils { | ||||
|     private static keyCounts: { keys: any, tags: any } = key_counts["default"] ?? key_counts | ||||
|     private static comparators | ||||
|         : [string, (a: number, b: number) => boolean][] | ||||
|         = [ | ||||
|     private static keyCounts: { keys: any; tags: any } = key_counts["default"] ?? key_counts | ||||
|     private static comparators: [string, (a: number, b: number) => boolean][] = [ | ||||
|         ["<=", (a, b) => a <= b], | ||||
|         [">=", (a, b) => a >= b], | ||||
|         ["<", (a, b) => a < b], | ||||
|  | @ -25,14 +23,14 @@ export class TagUtils { | |||
|     ] | ||||
| 
 | ||||
|     static KVtoProperties(tags: Tag[]): any { | ||||
|         const properties = {}; | ||||
|         const properties = {} | ||||
|         for (const tag of tags) { | ||||
|             properties[tag.key] = tag.value | ||||
|         } | ||||
|         return properties; | ||||
|         return properties | ||||
|     } | ||||
| 
 | ||||
|     static changeAsProperties(kvs: { k: string, v: string }[]): any { | ||||
|     static changeAsProperties(kvs: { k: string; v: string }[]): any { | ||||
|         const tags = {} | ||||
|         for (const kv of kvs) { | ||||
|             tags[kv.k] = kv.v | ||||
|  | @ -47,20 +45,20 @@ export class TagUtils { | |||
|         for (const neededKey in neededTags) { | ||||
|             const availableValues: string[] = availableTags[neededKey] | ||||
|             if (availableValues === undefined) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
|             const neededValues: string[] = neededTags[neededKey]; | ||||
|             const neededValues: string[] = neededTags[neededKey] | ||||
|             for (const neededValue of neededValues) { | ||||
|                 if (availableValues.indexOf(neededValue) < 0) { | ||||
|                     return false; | ||||
|                     return false | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return true; | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     static SplitKeys(tagsFilters: UploadableTag[]): Record<string, string[]> { | ||||
|         return <any>this.SplitKeysRegex(tagsFilters, false); | ||||
|         return <any>this.SplitKeysRegex(tagsFilters, false) | ||||
|     } | ||||
| 
 | ||||
|     /*** | ||||
|  | @ -68,68 +66,71 @@ export class TagUtils { | |||
|      * | ||||
|      * TagUtils.SplitKeysRegex([new Tag("isced:level", "bachelor; master")], true) // => {"isced:level": ["bachelor","master"]}
 | ||||
|      */ | ||||
|     static SplitKeysRegex(tagsFilters: UploadableTag[], allowRegex: boolean): Record<string, (string | RegexTag)[]> { | ||||
|     static SplitKeysRegex( | ||||
|         tagsFilters: UploadableTag[], | ||||
|         allowRegex: boolean | ||||
|     ): Record<string, (string | RegexTag)[]> { | ||||
|         const keyValues: Record<string, (string | RegexTag)[]> = {} | ||||
|         tagsFilters = [...tagsFilters] // copy all, use as queue
 | ||||
|         while (tagsFilters.length > 0) { | ||||
|             const tagsFilter = tagsFilters.shift(); | ||||
|             const tagsFilter = tagsFilters.shift() | ||||
| 
 | ||||
|             if (tagsFilter === undefined) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (tagsFilter instanceof And) { | ||||
|                 tagsFilters.push(...<UploadableTag[]>tagsFilter.and); | ||||
|                 continue; | ||||
|                 tagsFilters.push(...(<UploadableTag[]>tagsFilter.and)) | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (tagsFilter instanceof Tag) { | ||||
|                 if (keyValues[tagsFilter.key] === undefined) { | ||||
|                     keyValues[tagsFilter.key] = []; | ||||
|                     keyValues[tagsFilter.key] = [] | ||||
|                 } | ||||
|                 keyValues[tagsFilter.key].push(...tagsFilter.value.split(";").map(s => s.trim())); | ||||
|                 continue; | ||||
|                 keyValues[tagsFilter.key].push(...tagsFilter.value.split(";").map((s) => s.trim())) | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (allowRegex && tagsFilter instanceof RegexTag) { | ||||
|                 const key = tagsFilter.key | ||||
|                 if (isRegExp(key)) { | ||||
|                     console.error("Invalid type to flatten the multiAnswer: key is a regex too", tagsFilter); | ||||
|                     console.error( | ||||
|                         "Invalid type to flatten the multiAnswer: key is a regex too", | ||||
|                         tagsFilter | ||||
|                     ) | ||||
|                     throw "Invalid type to FlattenMultiAnswer" | ||||
|                 } | ||||
|                 const keystr = <string>key | ||||
|                 if (keyValues[keystr] === undefined) { | ||||
|                     keyValues[keystr] = []; | ||||
|                     keyValues[keystr] = [] | ||||
|                 } | ||||
|                 keyValues[keystr].push(tagsFilter); | ||||
|                 continue; | ||||
|                 keyValues[keystr].push(tagsFilter) | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             console.error("Invalid type to flatten the multiAnswer", tagsFilter); | ||||
|             console.error("Invalid type to flatten the multiAnswer", tagsFilter) | ||||
|             throw "Invalid type to FlattenMultiAnswer" | ||||
|         } | ||||
|         return keyValues; | ||||
|         return keyValues | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Flattens an 'uploadableTag' and replaces all 'SubstitutingTags' into normal tags | ||||
|      */ | ||||
|     static FlattenAnd(tagFilters: UploadableTag, currentProperties: Record<string, string>): Tag[]{ | ||||
|         const tags : Tag[] = [] | ||||
|     static FlattenAnd(tagFilters: UploadableTag, currentProperties: Record<string, string>): Tag[] { | ||||
|         const tags: Tag[] = [] | ||||
|         tagFilters.visit((tf: UploadableTag) => { | ||||
|             if(tf instanceof Tag){ | ||||
|             if (tf instanceof Tag) { | ||||
|                 tags.push(tf) | ||||
|             } | ||||
|             if(tf instanceof SubstitutingTag){ | ||||
|             if (tf instanceof SubstitutingTag) { | ||||
|                 tags.push(tf.asTag(currentProperties)) | ||||
|             } | ||||
|         }) | ||||
|         return tags | ||||
|     } | ||||
| 
 | ||||
|   | ||||
| 
 | ||||
|     /** | ||||
|      * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set. | ||||
|      * E.g: | ||||
|  | @ -152,17 +153,17 @@ export class TagUtils { | |||
|      */ | ||||
|     static FlattenMultiAnswer(tagsFilters: UploadableTag[]): And { | ||||
|         if (tagsFilters === undefined) { | ||||
|             return new And([]); | ||||
|             return new And([]) | ||||
|         } | ||||
| 
 | ||||
|         let keyValues = TagUtils.SplitKeys(tagsFilters); | ||||
|         let keyValues = TagUtils.SplitKeys(tagsFilters) | ||||
|         const and: UploadableTag[] = [] | ||||
|         for (const key in keyValues) { | ||||
|             const values = Utils.Dedup(keyValues[key]).filter(v => v !== "") | ||||
|             const values = Utils.Dedup(keyValues[key]).filter((v) => v !== "") | ||||
|             values.sort() | ||||
|             and.push(new Tag(key, values.join(";"))); | ||||
|             and.push(new Tag(key, values.join(";"))) | ||||
|         } | ||||
|         return new And(and); | ||||
|         return new And(and) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -177,16 +178,15 @@ export class TagUtils { | |||
|      * TagUtils.MatchesMultiAnswer(new Tag("isced:level","master"), {"isced:level":"bachelor; master"}) // => true
 | ||||
|      */ | ||||
|     static MatchesMultiAnswer(tag: UploadableTag, properties: Tags): boolean { | ||||
|         const splitted = TagUtils.SplitKeysRegex([tag], true); | ||||
|         const splitted = TagUtils.SplitKeysRegex([tag], true) | ||||
|         for (const splitKey in splitted) { | ||||
|             const neededValues = splitted[splitKey]; | ||||
|             const neededValues = splitted[splitKey] | ||||
|             if (properties[splitKey] === undefined) { | ||||
|                 return false; | ||||
|                 return false | ||||
|             } | ||||
| 
 | ||||
|             const actualValue = properties[splitKey].split(";").map(s => s.trim()); | ||||
|             const actualValue = properties[splitKey].split(";").map((s) => s.trim()) | ||||
|             for (const neededValue of neededValues) { | ||||
| 
 | ||||
|                 if (neededValue instanceof RegexTag) { | ||||
|                     if (!neededValue.matchesProperties(properties)) { | ||||
|                         return false | ||||
|  | @ -194,19 +194,19 @@ export class TagUtils { | |||
|                     continue | ||||
|                 } | ||||
|                 if (actualValue.indexOf(neededValue) < 0) { | ||||
|                     return false; | ||||
|                     return false | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return true; | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     public static SimpleTag(json: string, context?: string): Tag { | ||||
|         const tag = Utils.SplitFirst(json, "="); | ||||
|         const tag = Utils.SplitFirst(json, "=") | ||||
|         if (tag.length !== 2) { | ||||
|             throw `Invalid tag: no (or too much) '=' found (in ${context ?? "unkown context"})` | ||||
|         } | ||||
|         return new Tag(tag[0], tag[1]); | ||||
|         return new Tag(tag[0], tag[1]) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -269,30 +269,34 @@ export class TagUtils { | |||
|      */ | ||||
|     public static Tag(json: TagConfigJson, context: string = ""): TagsFilter { | ||||
|         try { | ||||
|             return this.ParseTagUnsafe(json, context); | ||||
|             return this.ParseTagUnsafe(json, context) | ||||
|         } catch (e) { | ||||
|             console.error("Could not parse tag", json, "in context", context, "due to ", e) | ||||
|             throw e; | ||||
|             throw e | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static ParseUploadableTag(json: TagConfigJson, context: string = ""): UploadableTag { | ||||
|             const t = this.Tag(json, context); | ||||
|         const t = this.Tag(json, context) | ||||
| 
 | ||||
|             t.visit((t : TagsFilter)=> { | ||||
|                 if( t instanceof  And){ | ||||
|         t.visit((t: TagsFilter) => { | ||||
|             if (t instanceof And) { | ||||
|                 return | ||||
|             } | ||||
|                 if(t instanceof Tag){ | ||||
|             if (t instanceof Tag) { | ||||
|                 return | ||||
|             } | ||||
|                 if(t instanceof SubstitutingTag){ | ||||
|             if (t instanceof SubstitutingTag) { | ||||
|                 return | ||||
|             } | ||||
|                 throw `Error at ${context}: detected a non-uploadable tag at a location where this is not supported: ${t.asHumanString(false, false, {})}` | ||||
|             throw `Error at ${context}: detected a non-uploadable tag at a location where this is not supported: ${t.asHumanString( | ||||
|                 false, | ||||
|                 false, | ||||
|                 {} | ||||
|             )}` | ||||
|         }) | ||||
| 
 | ||||
|             return <any> t | ||||
|         return <any>t | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -308,11 +312,10 @@ export class TagUtils { | |||
|         return TagUtils.Tag(json, context) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * INLINE sort of the given list | ||||
|      */ | ||||
|     public static sortFilters(filters: TagsFilter [], usePopularity: boolean): void { | ||||
|     public static sortFilters(filters: TagsFilter[], usePopularity: boolean): void { | ||||
|         filters.sort((a, b) => TagUtils.order(a, b, usePopularity)) | ||||
|     } | ||||
| 
 | ||||
|  | @ -346,42 +349,42 @@ export class TagUtils { | |||
|      * TagUtils.parseRegexOperator("tileId~*") // => {invert: false, key: "tileId", value: "*", modifier: ""}
 | ||||
|      */ | ||||
|     public static parseRegexOperator(tag: string): { | ||||
|         invert: boolean; | ||||
|         key: string; | ||||
|         value: string; | ||||
|         modifier: "i" | ""; | ||||
|         invert: boolean | ||||
|         key: string | ||||
|         value: string | ||||
|         modifier: "i" | "" | ||||
|     } | null { | ||||
|         const match = tag.match(/^([_a-zA-Z0-9: -]+)(!)?~([i]~)?(.*)$/); | ||||
|         const match = tag.match(/^([_a-zA-Z0-9: -]+)(!)?~([i]~)?(.*)$/) | ||||
|         if (match == null) { | ||||
|             return null; | ||||
|             return null | ||||
|         } | ||||
|         const [ , key, invert, modifier, value] = match; | ||||
|         return {key, value, invert: invert == "!", modifier: (modifier == "i~" ? "i" : "")}; | ||||
|         const [, key, invert, modifier, value] = match | ||||
|         return { key, value, invert: invert == "!", modifier: modifier == "i~" ? "i" : "" } | ||||
|     } | ||||
| 
 | ||||
|     private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilter { | ||||
| 
 | ||||
|         if (json === undefined) { | ||||
|             throw new Error(`Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`) | ||||
|             throw new Error( | ||||
|                 `Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression` | ||||
|             ) | ||||
|         } | ||||
|         if (typeof (json) != "string") { | ||||
|         if (typeof json != "string") { | ||||
|             if (json["and"] !== undefined && json["or"] !== undefined) { | ||||
|                 throw `Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined` | ||||
|             } | ||||
|             if (json["and"] !== undefined) { | ||||
|                 return new And(json["and"].map(t => TagUtils.Tag(t, context))); | ||||
|                 return new And(json["and"].map((t) => TagUtils.Tag(t, context))) | ||||
|             } | ||||
|             if (json["or"] !== undefined) { | ||||
|                 return new Or(json["or"].map(t => TagUtils.Tag(t, context))); | ||||
|                 return new Or(json["or"].map((t) => TagUtils.Tag(t, context))) | ||||
|             } | ||||
|             throw `At ${context}: unrecognized tag: ${JSON.stringify(json)}` | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const tag = json as string; | ||||
|         const tag = json as string | ||||
|         for (const [operator, comparator] of TagUtils.comparators) { | ||||
|             if (tag.indexOf(operator) >= 0) { | ||||
|                 const split = Utils.SplitFirst(tag, operator); | ||||
|                 const split = Utils.SplitFirst(tag, operator) | ||||
| 
 | ||||
|                 let val = Number(split[1].trim()) | ||||
|                 if (isNaN(val)) { | ||||
|  | @ -390,7 +393,7 @@ export class TagUtils { | |||
| 
 | ||||
|                 const f = (value: string | number | undefined) => { | ||||
|                     if (value === undefined) { | ||||
|                         return false; | ||||
|                         return false | ||||
|                     } | ||||
|                     let b: number | ||||
|                     if (typeof value === "number") { | ||||
|  | @ -413,14 +416,14 @@ export class TagUtils { | |||
|         } | ||||
| 
 | ||||
|         if (tag.indexOf("~~") >= 0) { | ||||
|             const split = Utils.SplitFirst(tag, "~~"); | ||||
|             const split = Utils.SplitFirst(tag, "~~") | ||||
|             if (split[1] === "*") { | ||||
|                 split[1] = "..*" | ||||
|             } | ||||
|             return new RegexTag( | ||||
|                 new RegExp("^" + split[0] + "$"), | ||||
|                 new RegExp("^" + split[1] + "$", "s") | ||||
|             ); | ||||
|             ) | ||||
|         } | ||||
|         const withRegex = TagUtils.parseRegexOperator(tag) | ||||
|         if (withRegex != null) { | ||||
|  | @ -428,10 +431,16 @@ export class TagUtils { | |||
|                 throw `Don't use 'key!~*' - use 'key=' instead (empty string as value (in the tag ${tag} while parsing ${context})` | ||||
|             } | ||||
|             if (withRegex.value === "") { | ||||
|                 throw "Detected a regextag with an empty regex; this is not allowed. Use '" + withRegex.key + "='instead (at " + context + ")" | ||||
|                 throw ( | ||||
|                     "Detected a regextag with an empty regex; this is not allowed. Use '" + | ||||
|                     withRegex.key + | ||||
|                     "='instead (at " + | ||||
|                     context + | ||||
|                     ")" | ||||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|             let value: string | RegExp = withRegex.value; | ||||
|             let value: string | RegExp = withRegex.value | ||||
|             if (value === "*") { | ||||
|                 value = "..*" | ||||
|             } | ||||
|  | @ -439,39 +448,40 @@ export class TagUtils { | |||
|                 withRegex.key, | ||||
|                 new RegExp("^" + value + "$", "s" + withRegex.modifier), | ||||
|                 withRegex.invert | ||||
|             ); | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         if (tag.indexOf("!:=") >= 0) { | ||||
|             const split = Utils.SplitFirst(tag, "!:="); | ||||
|             return new SubstitutingTag(split[0], split[1], true); | ||||
|             const split = Utils.SplitFirst(tag, "!:=") | ||||
|             return new SubstitutingTag(split[0], split[1], true) | ||||
|         } | ||||
|         if (tag.indexOf(":=") >= 0) { | ||||
|             const split = Utils.SplitFirst(tag, ":="); | ||||
|             return new SubstitutingTag(split[0], split[1]); | ||||
|             const split = Utils.SplitFirst(tag, ":=") | ||||
|             return new SubstitutingTag(split[0], split[1]) | ||||
|         } | ||||
| 
 | ||||
|         if (tag.indexOf("!=") >= 0) { | ||||
|             const split = Utils.SplitFirst(tag, "!="); | ||||
|             const split = Utils.SplitFirst(tag, "!=") | ||||
|             if (split[1] === "*") { | ||||
|                 throw "At " + context + ": invalid tag " + tag + ". To indicate a missing tag, use '" + split[0] + "!=' instead" | ||||
|                 throw ( | ||||
|                     "At " + | ||||
|                     context + | ||||
|                     ": invalid tag " + | ||||
|                     tag + | ||||
|                     ". To indicate a missing tag, use '" + | ||||
|                     split[0] + | ||||
|                     "!=' instead" | ||||
|                 ) | ||||
|             } | ||||
|             if (split[1] === "") { | ||||
|                 split[1] = "..*" | ||||
|                 return new RegexTag(split[0], /^..*$/s) | ||||
|             } | ||||
|             return new RegexTag( | ||||
|                 split[0], | ||||
|                 split[1], | ||||
|                 true | ||||
|             ); | ||||
|             return new RegexTag(split[0], split[1], true) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         if (tag.indexOf("=") >= 0) { | ||||
| 
 | ||||
| 
 | ||||
|             const split = Utils.SplitFirst(tag, "="); | ||||
|             const split = Utils.SplitFirst(tag, "=") | ||||
|             if (split[1] == "*") { | ||||
|                 throw `Error while parsing tag '${tag}' in ${context}: detected a wildcard on a normal value. Use a regex pattern instead` | ||||
|             } | ||||
|  | @ -524,7 +534,7 @@ export class TagUtils { | |||
|     } | ||||
| 
 | ||||
|     private static joinL(tfs: TagsFilter[], seperator: string, toplevel: boolean) { | ||||
|         const joined = tfs.map(e => TagUtils.toString(e, false)).join(seperator) | ||||
|         const joined = tfs.map((e) => TagUtils.toString(e, false)).join(seperator) | ||||
|         if (toplevel) { | ||||
|             return joined | ||||
|         } | ||||
|  | @ -542,14 +552,14 @@ export class TagUtils { | |||
|      * TagUtils.ContainsOppositeTags([new Tag("key", "value"), new RegexTag("key", "value", true)]) // => true
 | ||||
|      * TagUtils.ContainsOppositeTags([new Tag("key", "value"), new RegexTag("key", /value/, true)]) // => true
 | ||||
|      */ | ||||
|     public static ContainsOppositeTags(tags: (TagsFilter)[]): boolean { | ||||
|     public static ContainsOppositeTags(tags: TagsFilter[]): boolean { | ||||
|         for (let i = 0; i < tags.length; i++) { | ||||
|             const tag = tags[i]; | ||||
|             const tag = tags[i] | ||||
|             if (!(tag instanceof Tag || tag instanceof RegexTag)) { | ||||
|                 continue | ||||
|             } | ||||
|             for (let j = i + 1; j < tags.length; j++) { | ||||
|                 const guard = tags[j]; | ||||
|                 const guard = tags[j] | ||||
|                 if (!(guard instanceof Tag || guard instanceof RegexTag)) { | ||||
|                     continue | ||||
|                 } | ||||
|  | @ -579,8 +589,11 @@ export class TagUtils { | |||
|      * | ||||
|      * TagUtils.removeShadowedElementsFrom([new Tag("key","value")],  [new Tag("key","value"), new Tag("other_key","value")]) // => [new Tag("other_key","value")]
 | ||||
|      */ | ||||
|     public static removeShadowedElementsFrom(blacklist: TagsFilter[], listToFilter: TagsFilter[]): TagsFilter[] { | ||||
|         return listToFilter.filter(tf => !blacklist.some(guard => guard.shadows(tf))) | ||||
|     public static removeShadowedElementsFrom( | ||||
|         blacklist: TagsFilter[], | ||||
|         listToFilter: TagsFilter[] | ||||
|     ): TagsFilter[] { | ||||
|         return listToFilter.filter((tf) => !blacklist.some((guard) => guard.shadows(tf))) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -591,15 +604,15 @@ export class TagUtils { | |||
|     public static removeEquivalents(listToFilter: (Tag | RegexTag)[]): TagsFilter[] { | ||||
|         const result: TagsFilter[] = [] | ||||
|         outer: for (let i = 0; i < listToFilter.length; i++) { | ||||
|             const tag = listToFilter[i]; | ||||
|             const tag = listToFilter[i] | ||||
|             for (let j = 0; j < listToFilter.length; j++) { | ||||
|                 if (i === j) { | ||||
|                     continue | ||||
|                 } | ||||
|                 const guard = listToFilter[j]; | ||||
|                 const guard = listToFilter[j] | ||||
|                 if (guard.shadows(tag)) { | ||||
|                     // the guard 'kills' the tag: we continue the outer loop without adding the tag
 | ||||
|                     continue outer; | ||||
|                     continue outer | ||||
|                 } | ||||
|             } | ||||
|             result.push(tag) | ||||
|  | @ -615,10 +628,9 @@ export class TagUtils { | |||
|      * TagUtils.containsEquivalents([new Tag("key","value")],  [ new Tag("key","other_value")]) // => false
 | ||||
|      */ | ||||
|     public static containsEquivalents(guards: TagsFilter[], listToFilter: TagsFilter[]): boolean { | ||||
|         return listToFilter.some(tf => guards.some(guard => guard.shadows(tf))) | ||||
|         return listToFilter.some((tf) => guards.some((guard) => guard.shadows(tf))) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Parses a level specifier to the various available levels | ||||
|      * | ||||
|  | @ -633,13 +645,14 @@ export class TagUtils { | |||
|      */ | ||||
|     public static LevelsParser(level: string): string[] { | ||||
|         let spec = Utils.NoNull([level]) | ||||
|         spec = [].concat(...spec.map(s => s?.split(";"))) | ||||
|         spec = [].concat(...spec.map(s => { | ||||
|         spec = [].concat(...spec.map((s) => s?.split(";"))) | ||||
|         spec = [].concat( | ||||
|             ...spec.map((s) => { | ||||
|                 s = s.trim() | ||||
|                 if (s.indexOf("-") < 0 || s.startsWith("-")) { | ||||
|                     return s | ||||
|                 } | ||||
|             const [start, end] = s.split("-").map(s => Number(s.trim())) | ||||
|                 const [start, end] = s.split("-").map((s) => Number(s.trim())) | ||||
|                 if (isNaN(start) || isNaN(end)) { | ||||
|                     return undefined | ||||
|                 } | ||||
|  | @ -648,9 +661,8 @@ export class TagUtils { | |||
|                     values.push(i + "") | ||||
|                 } | ||||
|                 return values | ||||
|         })) | ||||
|         return Utils.NoNull(spec); | ||||
|             }) | ||||
|         ) | ||||
|         return Utils.NoNull(spec) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,26 +1,25 @@ | |||
| export abstract class TagsFilter { | ||||
| 
 | ||||
|     abstract asOverpass(): string[] | ||||
| 
 | ||||
|     abstract isUsableAsAnswer(): boolean; | ||||
|     abstract isUsableAsAnswer(): boolean | ||||
| 
 | ||||
|     /** | ||||
|      * Indicates some form of equivalency: | ||||
|      * if `this.shadows(t)`, then `this.matches(properties)` implies that `t.matches(properties)` for all possible properties | ||||
|      */ | ||||
|     abstract shadows(other: TagsFilter): boolean; | ||||
|     abstract shadows(other: TagsFilter): boolean | ||||
| 
 | ||||
|     abstract matchesProperties(properties: any): boolean; | ||||
|     abstract matchesProperties(properties: any): boolean | ||||
| 
 | ||||
|     abstract asHumanString(linkToWiki: boolean, shorten: boolean, properties: any): string; | ||||
|     abstract asHumanString(linkToWiki: boolean, shorten: boolean, properties: any): string | ||||
| 
 | ||||
|     abstract usedKeys(): string[]; | ||||
|     abstract usedKeys(): string[] | ||||
| 
 | ||||
|     /** | ||||
|      * Returns all normal key/value pairs | ||||
|      * Regex tags, substitutions, comparisons, ... are exempt | ||||
|      */ | ||||
|     abstract usedTags(): { key: string, value: string }[]; | ||||
|     abstract usedTags(): { key: string; value: string }[] | ||||
| 
 | ||||
|     /** | ||||
|      * Converts the tagsFilter into a list of key-values that should be uploaded to OSM. | ||||
|  | @ -28,12 +27,12 @@ export abstract class TagsFilter { | |||
|      * | ||||
|      * Note: properties are the already existing tags-object. It is only used in the substituting tag | ||||
|      */ | ||||
|     abstract asChange(properties: any): { k: string, v: string }[] | ||||
|     abstract asChange(properties: any): { k: string; v: string }[] | ||||
| 
 | ||||
|     /** | ||||
|      * Returns an optimized version (or self) of this tagsFilter | ||||
|      */ | ||||
|     abstract optimize(): TagsFilter | boolean; | ||||
|     abstract optimize(): TagsFilter | boolean | ||||
| 
 | ||||
|     /** | ||||
|      * Returns 'true' if the tagsfilter might select all features (i.e. the filter will return everything from OSM, except a few entries). | ||||
|  | @ -55,6 +54,5 @@ export abstract class TagsFilter { | |||
|     /** | ||||
|      * Walks the entire tree, every tagsFilter will be passed into the function once | ||||
|      */ | ||||
|     abstract visit(f: ((TagsFilter) => void)); | ||||
| 
 | ||||
|     abstract visit(f: (TagsFilter) => void) | ||||
| } | ||||
|  | @ -1,25 +1,27 @@ | |||
| import {Utils} from "../Utils"; | ||||
| import { Utils } from "../Utils" | ||||
| 
 | ||||
| /** | ||||
|  * Various static utils | ||||
|  */ | ||||
| export class Stores { | ||||
|     public static Chronic(millis: number, asLong: () => boolean = undefined): Store<Date> { | ||||
|         const source = new UIEventSource<Date>(undefined); | ||||
|         const source = new UIEventSource<Date>(undefined) | ||||
| 
 | ||||
|         function run() { | ||||
|             source.setData(new Date()); | ||||
|             source.setData(new Date()) | ||||
|             if (asLong === undefined || asLong()) { | ||||
|                 window.setTimeout(run, millis); | ||||
|                 window.setTimeout(run, millis) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         run(); | ||||
|         return source; | ||||
|         run() | ||||
|         return source | ||||
|     } | ||||
| 
 | ||||
|     public static FromPromiseWithErr<T>(promise: Promise<T>): Store<{ success: T } | { error: any }> { | ||||
|         return UIEventSource.FromPromiseWithErr(promise); | ||||
|     public static FromPromiseWithErr<T>( | ||||
|         promise: Promise<T> | ||||
|     ): Store<{ success: T } | { error: any }> { | ||||
|         return UIEventSource.FromPromiseWithErr(promise) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -30,13 +32,13 @@ export class Stores { | |||
|      */ | ||||
|     public static FromPromise<T>(promise: Promise<T>): Store<T> { | ||||
|         const src = new UIEventSource<T>(undefined) | ||||
|         promise?.then(d => src.setData(d)) | ||||
|         promise?.catch(err => console.warn("Promise failed:", err)) | ||||
|         promise?.then((d) => src.setData(d)) | ||||
|         promise?.catch((err) => console.warn("Promise failed:", err)) | ||||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     public static flatten<X>(source: Store<Store<X>>, possibleSources?: Store<any>[]): Store<X> { | ||||
|         return UIEventSource.flatten(source, possibleSources); | ||||
|         return UIEventSource.flatten(source, possibleSources) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -55,50 +57,49 @@ export class Stores { | |||
|      */ | ||||
|     public static ListStabilized<T>(src: Store<T[]>): Store<T[]> { | ||||
|         const stable = new UIEventSource<T[]>(undefined) | ||||
|         src.addCallbackAndRun(list => { | ||||
|         src.addCallbackAndRun((list) => { | ||||
|             if (list === undefined) { | ||||
|                 stable.setData(undefined) | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             const oldList = stable.data | ||||
|             if (oldList === list) { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             if(oldList == list){ | ||||
|                 return; | ||||
|             if (oldList == list) { | ||||
|                 return | ||||
|             } | ||||
|             if (oldList === undefined || oldList.length !== list.length) { | ||||
|                 stable.setData(list); | ||||
|                 return; | ||||
|                 stable.setData(list) | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             for (let i = 0; i < list.length; i++) { | ||||
|                 if (oldList[i] !== list[i]) { | ||||
|                     stable.setData(list); | ||||
|                     return; | ||||
|                     stable.setData(list) | ||||
|                     return | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // No actual changes, so we don't do anything
 | ||||
|             return; | ||||
|             return | ||||
|         }) | ||||
|         return stable | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export abstract class Store<T> { | ||||
|     abstract readonly data: T; | ||||
|     abstract readonly data: T | ||||
| 
 | ||||
|     /** | ||||
|      * OPtional value giving a title to the UIEventSource, mainly used for debugging | ||||
|      */ | ||||
|     public readonly tag: string | undefined; | ||||
| 
 | ||||
|     public readonly tag: string | undefined | ||||
| 
 | ||||
|     constructor(tag: string = undefined) { | ||||
|         this.tag = tag; | ||||
|         if ((tag === undefined || tag === "")) { | ||||
|             let createStack = Utils.runningFromConsole; | ||||
|         this.tag = tag | ||||
|         if (tag === undefined || tag === "") { | ||||
|             let createStack = Utils.runningFromConsole | ||||
|             if (!Utils.runningFromConsole) { | ||||
|                 createStack = window.location.hostname === "127.0.0.1" | ||||
|             } | ||||
|  | @ -109,43 +110,45 @@ export abstract class Store<T> { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     abstract map<J>(f: ((t: T) => J)): Store<J> | ||||
|     abstract map<J>(f: ((t: T) => J), extraStoresToWatch: Store<any>[]): Store<J> | ||||
|     abstract map<J>(f: (t: T) => J): Store<J> | ||||
|     abstract map<J>(f: (t: T) => J, extraStoresToWatch: Store<any>[]): Store<J> | ||||
| 
 | ||||
|     /** | ||||
|      * Add a callback function which will run on future data changes | ||||
|      */ | ||||
|     abstract addCallback(callback: (data: T) => void): (() => void); | ||||
|     abstract addCallback(callback: (data: T) => void): () => void | ||||
| 
 | ||||
|     /** | ||||
|      * Adds a callback function, which will be run immediately. | ||||
|      * Only triggers if the current data is defined | ||||
|      */ | ||||
|     abstract addCallbackAndRunD(callback: (data: T) => void): (() => void); | ||||
|     abstract addCallbackAndRunD(callback: (data: T) => void): () => void | ||||
| 
 | ||||
|     /** | ||||
|      * Add a callback function which will run on future data changes | ||||
|      * Only triggers if the data is defined | ||||
|      */ | ||||
|     abstract addCallbackD(callback: (data: T) => void): (() => void); | ||||
|     abstract addCallbackD(callback: (data: T) => void): () => void | ||||
| 
 | ||||
|     /** | ||||
|      * Adds a callback function, which will be run immediately. | ||||
|      * Only triggers if the current data is defined | ||||
|      */ | ||||
|     abstract addCallbackAndRun(callback: (data: T) => void): (() => void); | ||||
|     abstract addCallbackAndRun(callback: (data: T) => void): () => void | ||||
| 
 | ||||
|     public withEqualityStabilized(comparator: (t: T | undefined, t1: T | undefined) => boolean): Store<T> { | ||||
|         let oldValue = undefined; | ||||
|         return this.map(v => { | ||||
|     public withEqualityStabilized( | ||||
|         comparator: (t: T | undefined, t1: T | undefined) => boolean | ||||
|     ): Store<T> { | ||||
|         let oldValue = undefined | ||||
|         return this.map((v) => { | ||||
|             if (v == oldValue) { | ||||
|                 return oldValue | ||||
|             } | ||||
|             if (comparator(oldValue, v)) { | ||||
|                 return oldValue | ||||
|             } | ||||
|             oldValue = v; | ||||
|             return v; | ||||
|             oldValue = v | ||||
|             return v | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|  | @ -195,20 +198,20 @@ export abstract class Store<T> { | |||
|      * src.setData(0) | ||||
|      * lastValue // => "def"
 | ||||
|      */ | ||||
|     public bind<X>(f: ((t: T) => Store<X>)): Store<X> { | ||||
|     public bind<X>(f: (t: T) => Store<X>): Store<X> { | ||||
|         const mapped = this.map(f) | ||||
|         const sink = new UIEventSource<X>(undefined) | ||||
|         const seenEventSources = new Set<Store<X>>(); | ||||
|         mapped.addCallbackAndRun(newEventSource => { | ||||
|         const seenEventSources = new Set<Store<X>>() | ||||
|         mapped.addCallbackAndRun((newEventSource) => { | ||||
|             if (newEventSource === null) { | ||||
|                 sink.setData(null) | ||||
|             } else if (newEventSource === undefined) { | ||||
|                 sink.setData(undefined) | ||||
|             } else if (!seenEventSources.has(newEventSource)) { | ||||
|                 seenEventSources.add(newEventSource) | ||||
|                 newEventSource.addCallbackAndRun(resultData => { | ||||
|                 newEventSource.addCallbackAndRun((resultData) => { | ||||
|                     if (mapped.data === newEventSource) { | ||||
|                         sink.setData(resultData); | ||||
|                         sink.setData(resultData) | ||||
|                     } | ||||
|                 }) | ||||
|             } else { | ||||
|  | @ -217,67 +220,66 @@ export abstract class Store<T> { | |||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         return sink; | ||||
|         return sink | ||||
|     } | ||||
| 
 | ||||
|     public stabilized(millisToStabilize): Store<T> { | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return this; | ||||
|             return this | ||||
|         } | ||||
| 
 | ||||
|         const newSource = new UIEventSource<T>(this.data); | ||||
|         const newSource = new UIEventSource<T>(this.data) | ||||
| 
 | ||||
|         this.addCallback(latestData => { | ||||
|         this.addCallback((latestData) => { | ||||
|             window.setTimeout(() => { | ||||
|                 if (this.data == latestData) { // compare by reference
 | ||||
|                     newSource.setData(latestData); | ||||
|                 if (this.data == latestData) { | ||||
|                     // compare by reference
 | ||||
|                     newSource.setData(latestData) | ||||
|                 } | ||||
|             }, millisToStabilize) | ||||
|         }); | ||||
|         }) | ||||
| 
 | ||||
|         return newSource; | ||||
|         return newSource | ||||
|     } | ||||
| 
 | ||||
|     public AsPromise(condition?: ((t: T) => boolean)): Promise<T> { | ||||
|         const self = this; | ||||
|         condition = condition ?? (t => t !== undefined) | ||||
|     public AsPromise(condition?: (t: T) => boolean): Promise<T> { | ||||
|         const self = this | ||||
|         condition = condition ?? ((t) => t !== undefined) | ||||
|         return new Promise((resolve) => { | ||||
|             if (condition(self.data)) { | ||||
|                 resolve(self.data) | ||||
|             } else { | ||||
|                 self.addCallbackD(data => { | ||||
|                 self.addCallbackD((data) => { | ||||
|                     resolve(data) | ||||
|                     return true; // return true to unregister as we only need to be called once
 | ||||
|                     return true // return true to unregister as we only need to be called once
 | ||||
|                 }) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class ImmutableStore<T> extends Store<T> { | ||||
|     public readonly data: T; | ||||
|     public readonly data: T | ||||
| 
 | ||||
|     private static readonly pass: (() => void) = () => { | ||||
|     } | ||||
|     private static readonly pass: () => void = () => {} | ||||
| 
 | ||||
|     constructor(data: T) { | ||||
|         super(); | ||||
|         this.data = data; | ||||
|         super() | ||||
|         this.data = data | ||||
|     } | ||||
| 
 | ||||
|     addCallback(callback: (data: T) => void): (() => void) { | ||||
|     addCallback(callback: (data: T) => void): () => void { | ||||
|         // pass: data will never change
 | ||||
|         return ImmutableStore.pass | ||||
|     } | ||||
| 
 | ||||
|     addCallbackAndRun(callback: (data: T) => void): (() => void) { | ||||
|     addCallbackAndRun(callback: (data: T) => void): () => void { | ||||
|         callback(this.data) | ||||
|         // no callback registry: data will never change
 | ||||
|         return ImmutableStore.pass | ||||
|     } | ||||
| 
 | ||||
|     addCallbackAndRunD(callback: (data: T) => void): (() => void) { | ||||
|     addCallbackAndRunD(callback: (data: T) => void): () => void { | ||||
|         if (this.data !== undefined) { | ||||
|             callback(this.data) | ||||
|         } | ||||
|  | @ -285,38 +287,35 @@ export class ImmutableStore<T> extends Store<T> { | |||
|         return ImmutableStore.pass | ||||
|     } | ||||
| 
 | ||||
|     addCallbackD(callback: (data: T) => void): (() => void) { | ||||
|     addCallbackD(callback: (data: T) => void): () => void { | ||||
|         // pass: data will never change
 | ||||
|         return ImmutableStore.pass | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     map<J>(f: (t: T) => J, extraStores: Store<any>[] = undefined): ImmutableStore<J> { | ||||
|         if(extraStores?.length > 0){ | ||||
|         if (extraStores?.length > 0) { | ||||
|             return new MappedStore(this, f, extraStores, undefined, f(this.data)) | ||||
|         } | ||||
|         return new ImmutableStore<J>(f(this.data)); | ||||
|         return new ImmutableStore<J>(f(this.data)) | ||||
|     } | ||||
| 
 | ||||
|      | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Keeps track of the callback functions | ||||
|  */ | ||||
| class ListenerTracker<T> { | ||||
|     private readonly _callbacks: ((t: T) => (boolean | void | any)) [] = []; | ||||
|     private readonly _callbacks: ((t: T) => boolean | void | any)[] = [] | ||||
| 
 | ||||
|     public pingCount = 0; | ||||
|     public pingCount = 0 | ||||
|     /** | ||||
|      * Adds a callback which can be called; a function to unregister is returned | ||||
|      */ | ||||
|     public addCallback(callback: (t: T) => (boolean | void | any)): (() => void) { | ||||
|     public addCallback(callback: (t: T) => boolean | void | any): () => void { | ||||
|         if (callback === console.log) { | ||||
|             // This ^^^ actually works!
 | ||||
|             throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead." | ||||
|         } | ||||
|         this._callbacks.push(callback); | ||||
|         this._callbacks.push(callback) | ||||
| 
 | ||||
|         // Give back an unregister-function!
 | ||||
|         return () => { | ||||
|  | @ -332,9 +331,9 @@ class ListenerTracker<T> { | |||
|      * Returns the number of registered callbacks | ||||
|      */ | ||||
|     public ping(data: T): number { | ||||
|         this.pingCount ++; | ||||
|         this.pingCount++ | ||||
|         let toDelete = undefined | ||||
|         let startTime = new Date().getTime() / 1000; | ||||
|         let startTime = new Date().getTime() / 1000 | ||||
|         for (const callback of this._callbacks) { | ||||
|             if (callback(data) === true) { | ||||
|                 // This callback wants to be deleted
 | ||||
|  | @ -347,8 +346,10 @@ class ListenerTracker<T> { | |||
|             } | ||||
|         } | ||||
|         let endTime = new Date().getTime() / 1000 | ||||
|         if ((endTime - startTime) > 500) { | ||||
|             console.trace("Warning: a ping took more then 500ms; this is probably a performance issue") | ||||
|         if (endTime - startTime > 500) { | ||||
|             console.trace( | ||||
|                 "Warning: a ping took more then 500ms; this is probably a performance issue" | ||||
|             ) | ||||
|         } | ||||
|         if (toDelete !== undefined) { | ||||
|             for (const toDeleteElement of toDelete) { | ||||
|  | @ -363,40 +364,42 @@ class ListenerTracker<T> { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * The mapped store is a helper type which does the mapping of a function. | ||||
|  * It'll fuse | ||||
|  */ | ||||
| class MappedStore<TIn, T> extends Store<T> { | ||||
|     private _upstream: Store<TIn> | ||||
|     private _upstreamCallbackHandler: ListenerTracker<TIn> | undefined | ||||
|     private _upstreamPingCount: number = -1 | ||||
|     private _unregisterFromUpstream: () => void | ||||
| 
 | ||||
|     private _upstream: Store<TIn>; | ||||
|     private _upstreamCallbackHandler: ListenerTracker<TIn> | undefined; | ||||
|     private _upstreamPingCount: number = -1; | ||||
|     private _unregisterFromUpstream: (() => void) | ||||
|      | ||||
|     private _f: (t: TIn) => T; | ||||
|     private readonly _extraStores: Store<any>[] | undefined; | ||||
|     private _f: (t: TIn) => T | ||||
|     private readonly _extraStores: Store<any>[] | undefined | ||||
|     private _unregisterFromExtraStores: (() => void)[] | undefined | ||||
| 
 | ||||
|     private _callbacks: ListenerTracker<T> = new ListenerTracker<T>() | ||||
| 
 | ||||
|     private static readonly pass: () => {} | ||||
| 
 | ||||
| 
 | ||||
|     constructor(upstream: Store<TIn>, f: (t: TIn) => T, extraStores: Store<any>[],  | ||||
|                 upstreamListenerHandler: ListenerTracker<TIn> | undefined, initialState: T) { | ||||
|         super(); | ||||
|         this._upstream = upstream; | ||||
|     constructor( | ||||
|         upstream: Store<TIn>, | ||||
|         f: (t: TIn) => T, | ||||
|         extraStores: Store<any>[], | ||||
|         upstreamListenerHandler: ListenerTracker<TIn> | undefined, | ||||
|         initialState: T | ||||
|     ) { | ||||
|         super() | ||||
|         this._upstream = upstream | ||||
|         this._upstreamCallbackHandler = upstreamListenerHandler | ||||
|         this._f = f; | ||||
|         this._f = f | ||||
|         this._data = initialState | ||||
|         this._upstreamPingCount = upstreamListenerHandler?.pingCount | ||||
|         this._extraStores = extraStores; | ||||
|         this._extraStores = extraStores | ||||
|         this.registerCallbacksToUpstream() | ||||
|     } | ||||
| 
 | ||||
|     private _data: T; | ||||
|     private _data: T | ||||
|     private _callbacksAreRegistered = false | ||||
| 
 | ||||
|     /** | ||||
|  | @ -411,7 +414,7 @@ class MappedStore<TIn, T> extends Store<T> { | |||
|     get data(): T { | ||||
|         if (!this._callbacksAreRegistered) { | ||||
|             // Callbacks are not registered, so we haven't been listening for updates from the upstream which might have changed
 | ||||
|             if(this._upstreamCallbackHandler?.pingCount != this._upstreamPingCount){ | ||||
|             if (this._upstreamCallbackHandler?.pingCount != this._upstreamPingCount) { | ||||
|                 // Upstream has pinged - let's update our data first
 | ||||
|                 this._data = this._f(this._upstream.data) | ||||
|             } | ||||
|  | @ -420,8 +423,7 @@ class MappedStore<TIn, T> extends Store<T> { | |||
|         return this._data | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     map<J>(f: (t: T) => J, extraStores: (Store<any>)[] = undefined): Store<J> { | ||||
|     map<J>(f: (t: T) => J, extraStores: Store<any>[] = undefined): Store<J> { | ||||
|         let stores: Store<any>[] = undefined | ||||
|         if (extraStores?.length > 0 || this._extraStores?.length > 0) { | ||||
|             stores = [] | ||||
|  | @ -430,7 +432,7 @@ class MappedStore<TIn, T> extends Store<T> { | |||
|             stores.push(...extraStores) | ||||
|         } | ||||
|         if (this._extraStores?.length > 0) { | ||||
|             this._extraStores?.forEach(store => { | ||||
|             this._extraStores?.forEach((store) => { | ||||
|                 if (stores.indexOf(store) < 0) { | ||||
|                     stores.push(store) | ||||
|                 } | ||||
|  | @ -442,39 +444,37 @@ class MappedStore<TIn, T> extends Store<T> { | |||
|             stores, | ||||
|             this._callbacks, | ||||
|             f(this.data) | ||||
|         ); | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private unregisterFromUpstream() { | ||||
|         console.log("Unregistering callbacks for", this.tag) | ||||
|         this._callbacksAreRegistered = false; | ||||
|         this._callbacksAreRegistered = false | ||||
|         this._unregisterFromUpstream() | ||||
|         this._unregisterFromExtraStores?.forEach(unr => unr()) | ||||
|         this._unregisterFromExtraStores?.forEach((unr) => unr()) | ||||
|     } | ||||
| 
 | ||||
|     private registerCallbacksToUpstream() { | ||||
|         const self = this | ||||
| 
 | ||||
|         this._unregisterFromUpstream = this._upstream.addCallback( | ||||
|             _ => self.update() | ||||
|         this._unregisterFromUpstream = this._upstream.addCallback((_) => self.update()) | ||||
|         this._unregisterFromExtraStores = this._extraStores?.map((store) => | ||||
|             store?.addCallback((_) => self.update()) | ||||
|         ) | ||||
|         this._unregisterFromExtraStores = this._extraStores?.map(store => | ||||
|             store?.addCallback(_ => self.update()) | ||||
|         ) | ||||
|         this._callbacksAreRegistered = true; | ||||
|         this._callbacksAreRegistered = true | ||||
|     } | ||||
| 
 | ||||
|     private update(): void { | ||||
|         const newData = this._f(this._upstream.data) | ||||
|         this._upstreamPingCount = this._upstreamCallbackHandler?.pingCount | ||||
|         if (this._data == newData) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         this._data = newData | ||||
|         this._callbacks.ping(this._data) | ||||
|     } | ||||
| 
 | ||||
|     addCallback(callback: (data: T) => (any | boolean | void)): (() => void) { | ||||
|     addCallback(callback: (data: T) => any | boolean | void): () => void { | ||||
|         if (!this._callbacksAreRegistered) { | ||||
|             // This is the first callback that is added
 | ||||
|             // We register this 'map' to the upstream object and all the streams
 | ||||
|  | @ -489,7 +489,7 @@ class MappedStore<TIn, T> extends Store<T> { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     addCallbackAndRun(callback: (data: T) => (any | boolean | void)): (() => void) { | ||||
|     addCallbackAndRun(callback: (data: T) => any | boolean | void): () => void { | ||||
|         const unregister = this.addCallback(callback) | ||||
|         const doRemove = callback(this.data) | ||||
|         if (doRemove === true) { | ||||
|  | @ -499,71 +499,74 @@ class MappedStore<TIn, T> extends Store<T> { | |||
|         return unregister | ||||
|     } | ||||
| 
 | ||||
|     addCallbackAndRunD(callback: (data: T) => (any | boolean | void)): (() => void) { | ||||
|         return this.addCallbackAndRun(data => { | ||||
|     addCallbackAndRunD(callback: (data: T) => any | boolean | void): () => void { | ||||
|         return this.addCallbackAndRun((data) => { | ||||
|             if (data !== undefined) { | ||||
|                 return callback(data) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     addCallbackD(callback: (data: T) => (any | boolean | void)): (() => void) { | ||||
|         return this.addCallback(data => { | ||||
|     addCallbackD(callback: (data: T) => any | boolean | void): () => void { | ||||
|         return this.addCallback((data) => { | ||||
|             if (data !== undefined) { | ||||
|                 return callback(data) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class UIEventSource<T> extends Store<T> { | ||||
| 
 | ||||
|     public data: T; | ||||
|     public data: T | ||||
|     _callbacks: ListenerTracker<T> = new ListenerTracker<T>() | ||||
| 
 | ||||
|     private static readonly pass: () => {} | ||||
| 
 | ||||
|     constructor(data: T, tag: string = "") { | ||||
|         super(tag); | ||||
|         this.data = data; | ||||
|         super(tag) | ||||
|         this.data = data | ||||
|     } | ||||
| 
 | ||||
|     public static flatten<X>(source: Store<Store<X>>, possibleSources?: Store<any>[]): UIEventSource<X> { | ||||
|         const sink = new UIEventSource<X>(source.data?.data); | ||||
|     public static flatten<X>( | ||||
|         source: Store<Store<X>>, | ||||
|         possibleSources?: Store<any>[] | ||||
|     ): UIEventSource<X> { | ||||
|         const sink = new UIEventSource<X>(source.data?.data) | ||||
| 
 | ||||
|         source.addCallback((latestData) => { | ||||
|             sink.setData(latestData?.data); | ||||
|             latestData.addCallback(data => { | ||||
|             sink.setData(latestData?.data) | ||||
|             latestData.addCallback((data) => { | ||||
|                 if (source.data !== latestData) { | ||||
|                     return true; | ||||
|                     return true | ||||
|                 } | ||||
|                 sink.setData(data) | ||||
|             }) | ||||
|         }); | ||||
|         }) | ||||
| 
 | ||||
|         for (const possibleSource of possibleSources ?? []) { | ||||
|             possibleSource?.addCallback(() => { | ||||
|                 sink.setData(source.data?.data); | ||||
|                 sink.setData(source.data?.data) | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         return sink; | ||||
|         return sink | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated. | ||||
|      * If the promise fails, the value will stay undefined, but 'onError' will be called | ||||
|      */ | ||||
|     public static FromPromise<T>(promise: Promise<T>, onError: ((e: any) => void) = undefined): UIEventSource<T> { | ||||
|     public static FromPromise<T>( | ||||
|         promise: Promise<T>, | ||||
|         onError: (e: any) => void = undefined | ||||
|     ): UIEventSource<T> { | ||||
|         const src = new UIEventSource<T>(undefined) | ||||
|         promise?.then(d => src.setData(d)) | ||||
|         promise?.catch(err => { | ||||
|         promise?.then((d) => src.setData(d)) | ||||
|         promise?.catch((err) => { | ||||
|             if (onError !== undefined) { | ||||
|                 onError(err) | ||||
|             } else { | ||||
|                 console.warn("Promise failed:", err); | ||||
|                 console.warn("Promise failed:", err) | ||||
|             } | ||||
|         }) | ||||
|         return src | ||||
|  | @ -575,25 +578,27 @@ export class UIEventSource<T> extends Store<T> { | |||
|      * @param promise | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static FromPromiseWithErr<T>(promise: Promise<T>): UIEventSource<{ success: T } | { error: any }> { | ||||
|     public static FromPromiseWithErr<T>( | ||||
|         promise: Promise<T> | ||||
|     ): UIEventSource<{ success: T } | { error: any }> { | ||||
|         const src = new UIEventSource<{ success: T } | { error: any }>(undefined) | ||||
|         promise?.then(d => src.setData({success: d})) | ||||
|         promise?.catch(err => src.setData({error: err})) | ||||
|         promise?.then((d) => src.setData({ success: d })) | ||||
|         promise?.catch((err) => src.setData({ error: err })) | ||||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     public static asFloat(source: UIEventSource<string>): UIEventSource<number> { | ||||
|         return source.sync( | ||||
|             (str) => { | ||||
|                 let parsed = parseFloat(str); | ||||
|                 return isNaN(parsed) ? undefined : parsed; | ||||
|                 let parsed = parseFloat(str) | ||||
|                 return isNaN(parsed) ? undefined : parsed | ||||
|             }, | ||||
|             [], | ||||
|             (fl) => { | ||||
|                 if (fl === undefined || isNaN(fl)) { | ||||
|                     return undefined; | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return ("" + fl).substr(0, 8); | ||||
|                 return ("" + fl).substr(0, 8) | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
|  | @ -604,29 +609,29 @@ export class UIEventSource<T> extends Store<T> { | |||
|      * If the result of the callback is 'true', the callback is considered finished and will be removed again | ||||
|      * @param callback | ||||
|      */ | ||||
|     public addCallback(callback: ((latestData: T) => (boolean | void | any))): (() => void) { | ||||
|         return this._callbacks.addCallback(callback); | ||||
|     public addCallback(callback: (latestData: T) => boolean | void | any): () => void { | ||||
|         return this._callbacks.addCallback(callback) | ||||
|     } | ||||
| 
 | ||||
|     public addCallbackAndRun(callback: ((latestData: T) => (boolean | void | any))): (() => void) { | ||||
|         const doDeleteCallback = callback(this.data); | ||||
|     public addCallbackAndRun(callback: (latestData: T) => boolean | void | any): () => void { | ||||
|         const doDeleteCallback = callback(this.data) | ||||
|         if (doDeleteCallback !== true) { | ||||
|             return this.addCallback(callback); | ||||
|             return this.addCallback(callback) | ||||
|         } else { | ||||
|             return UIEventSource.pass | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public addCallbackAndRunD(callback: (data: T) => void): (() => void) { | ||||
|         return this.addCallbackAndRun(data => { | ||||
|     public addCallbackAndRunD(callback: (data: T) => void): () => void { | ||||
|         return this.addCallbackAndRun((data) => { | ||||
|             if (data !== undefined && data !== null) { | ||||
|                 return callback(data) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public addCallbackD(callback: (data: T) => void): (() => void) { | ||||
|         return this.addCallback(data => { | ||||
|     public addCallbackD(callback: (data: T) => void): () => void { | ||||
|         return this.addCallback((data) => { | ||||
|             if (data !== undefined && data !== null) { | ||||
|                 return callback(data) | ||||
|             } | ||||
|  | @ -634,12 +639,13 @@ export class UIEventSource<T> extends Store<T> { | |||
|     } | ||||
| 
 | ||||
|     public setData(t: T): UIEventSource<T> { | ||||
|         if (this.data == t) { // MUST COMPARE BY REFERENCE!
 | ||||
|             return; | ||||
|         if (this.data == t) { | ||||
|             // MUST COMPARE BY REFERENCE!
 | ||||
|             return | ||||
|         } | ||||
|         this.data = t; | ||||
|         this.data = t | ||||
|         this._callbacks.ping(t) | ||||
|         return this; | ||||
|         return this | ||||
|     } | ||||
| 
 | ||||
|     public ping(): void { | ||||
|  | @ -669,9 +675,8 @@ export class UIEventSource<T> extends Store<T> { | |||
|      * srcSeen // => 21
 | ||||
|      * lastSeen // => 42
 | ||||
|      */ | ||||
|     public map<J>(f: ((t: T) => J), | ||||
|                   extraSources: Store<any>[] = []): Store<J> { | ||||
|         return new MappedStore(this, f, extraSources, this._callbacks, f(this.data)); | ||||
|     public map<J>(f: (t: T) => J, extraSources: Store<any>[] = []): Store<J> { | ||||
|         return new MappedStore(this, f, extraSources, this._callbacks, f(this.data)) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -682,53 +687,51 @@ export class UIEventSource<T> extends Store<T> { | |||
|      * @param g: a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData | ||||
|      * @param allowUnregister: if set, the update will be halted if no listeners are registered | ||||
|      */ | ||||
|     public sync<J>(f: ((t: T) => J), | ||||
|     public sync<J>( | ||||
|         f: (t: T) => J, | ||||
|         extraSources: Store<any>[], | ||||
|                    g: ((j: J, t: T) => T), | ||||
|                    allowUnregister = false): UIEventSource<J> { | ||||
|         const self = this; | ||||
|         g: (j: J, t: T) => T, | ||||
|         allowUnregister = false | ||||
|     ): UIEventSource<J> { | ||||
|         const self = this | ||||
| 
 | ||||
|         const stack = new Error().stack.split("\n"); | ||||
|         const stack = new Error().stack.split("\n") | ||||
|         const callee = stack[1] | ||||
| 
 | ||||
|         const newSource = new UIEventSource<J>( | ||||
|             f(this.data), | ||||
|             "map(" + this.tag + ")@" + callee | ||||
|         ); | ||||
|         const newSource = new UIEventSource<J>(f(this.data), "map(" + this.tag + ")@" + callee) | ||||
| 
 | ||||
|         const update = function () { | ||||
|             newSource.setData(f(self.data)); | ||||
|             newSource.setData(f(self.data)) | ||||
|             return allowUnregister && newSource._callbacks.length() === 0 | ||||
|         } | ||||
| 
 | ||||
|         this.addCallback(update); | ||||
|         this.addCallback(update) | ||||
|         for (const extraSource of extraSources) { | ||||
|             extraSource?.addCallback(update); | ||||
|             extraSource?.addCallback(update) | ||||
|         } | ||||
| 
 | ||||
|         if (g !== undefined) { | ||||
|             newSource.addCallback((latest) => { | ||||
|                 self.setData(g(latest, self.data)); | ||||
|                 self.setData(g(latest, self.data)) | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         return newSource; | ||||
|         return newSource | ||||
|     } | ||||
| 
 | ||||
|     public syncWith(otherSource: UIEventSource<T>, reverseOverride = false): UIEventSource<T> { | ||||
|         this.addCallback((latest) => otherSource.setData(latest)); | ||||
|         const self = this; | ||||
|         otherSource.addCallback((latest) => self.setData(latest)); | ||||
|         this.addCallback((latest) => otherSource.setData(latest)) | ||||
|         const self = this | ||||
|         otherSource.addCallback((latest) => self.setData(latest)) | ||||
|         if (reverseOverride) { | ||||
|             if (otherSource.data !== undefined) { | ||||
|                 this.setData(otherSource.data); | ||||
|                 this.setData(otherSource.data) | ||||
|             } | ||||
|         } else if (this.data === undefined) { | ||||
|             this.setData(otherSource.data); | ||||
|             this.setData(otherSource.data) | ||||
|         } else { | ||||
|             otherSource.setData(this.data); | ||||
|             otherSource.setData(this.data) | ||||
|         } | ||||
|         return this; | ||||
|         return this | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,12 +1,11 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| /** | ||||
|  * Wrapper around the hash to create an UIEventSource from it | ||||
|  */ | ||||
| export default class Hash { | ||||
| 
 | ||||
|     public static hash: UIEventSource<string> = Hash.Get(); | ||||
|     public static hash: UIEventSource<string> = Hash.Get() | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the current string, including the pound sign if there is any | ||||
|  | @ -16,48 +15,46 @@ export default class Hash { | |||
|         if (Hash.hash.data === undefined || Hash.hash.data === "") { | ||||
|             return "" | ||||
|         } else { | ||||
|             return "#" + Hash.hash.data; | ||||
|             return "#" + Hash.hash.data | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static Get(): UIEventSource<string> { | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return new UIEventSource<string>(undefined); | ||||
|             return new UIEventSource<string>(undefined) | ||||
|         } | ||||
|         const hash = new UIEventSource<string>(window.location.hash.substr(1)); | ||||
|         hash.addCallback(h => { | ||||
|         const hash = new UIEventSource<string>(window.location.hash.substr(1)) | ||||
|         hash.addCallback((h) => { | ||||
|             if (h === "undefined") { | ||||
|                 console.warn("Got a literal 'undefined' as hash, ignoring") | ||||
|                 h = undefined; | ||||
|                 h = undefined | ||||
|             } | ||||
| 
 | ||||
|             if (h === undefined || h === "") { | ||||
|                 window.location.hash = ""; | ||||
|                 return; | ||||
|                 window.location.hash = "" | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             history.pushState({}, "") | ||||
|             window.location.hash = "#" + h; | ||||
|         }); | ||||
| 
 | ||||
|             window.location.hash = "#" + h | ||||
|         }) | ||||
| 
 | ||||
|         window.onhashchange = () => { | ||||
|             let newValue = window.location.hash.substr(1); | ||||
|             let newValue = window.location.hash.substr(1) | ||||
|             if (newValue === "") { | ||||
|                 newValue = undefined; | ||||
|                 newValue = undefined | ||||
|             } | ||||
|             hash.setData(newValue) | ||||
|         } | ||||
| 
 | ||||
|         window.addEventListener('popstate', _ => { | ||||
|             let newValue = window.location.hash.substr(1); | ||||
|         window.addEventListener("popstate", (_) => { | ||||
|             let newValue = window.location.hash.substr(1) | ||||
|             if (newValue === "") { | ||||
|                 newValue = undefined; | ||||
|                 newValue = undefined | ||||
|             } | ||||
|             hash.setData(newValue) | ||||
|         }) | ||||
| 
 | ||||
|         return hash; | ||||
|         return hash | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,38 +1,41 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import * as idb from "idb-keyval" | ||||
| import {Utils} from "../../Utils"; | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| /** | ||||
|  * UIEventsource-wrapper around indexedDB key-value | ||||
|  */ | ||||
| export class IdbLocalStorage { | ||||
| 
 | ||||
|     private static readonly _sourceCache: Record<string, UIEventSource<any>> = {} | ||||
| 
 | ||||
|     public static Get<T>(key: string, options?: { defaultValue?: T, whenLoaded?: (t: T | null) => void }): UIEventSource<T> { | ||||
|         if(IdbLocalStorage._sourceCache[key] !== undefined){ | ||||
|     public static Get<T>( | ||||
|         key: string, | ||||
|         options?: { defaultValue?: T; whenLoaded?: (t: T | null) => void } | ||||
|     ): UIEventSource<T> { | ||||
|         if (IdbLocalStorage._sourceCache[key] !== undefined) { | ||||
|             return IdbLocalStorage._sourceCache[key] | ||||
|         } | ||||
|         const src = new UIEventSource<T>(options?.defaultValue, "idb-local-storage:" + key) | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return src; | ||||
|             return src | ||||
|         } | ||||
|         src.addCallback(v => idb.set(key, v)) | ||||
|         src.addCallback((v) => idb.set(key, v)) | ||||
| 
 | ||||
|         idb.get(key).then(v => { | ||||
|             src.setData(v ?? options?.defaultValue); | ||||
|         idb.get(key) | ||||
|             .then((v) => { | ||||
|                 src.setData(v ?? options?.defaultValue) | ||||
|                 if (options?.whenLoaded !== undefined) { | ||||
|                     options?.whenLoaded(v) | ||||
|                 } | ||||
|         }).catch(err => { | ||||
|             }) | ||||
|             .catch((err) => { | ||||
|                 console.warn("Loading from local storage failed due to", err) | ||||
|                 if (options?.whenLoaded !== undefined) { | ||||
|                     options?.whenLoaded(null) | ||||
|                 } | ||||
|             }) | ||||
|         IdbLocalStorage._sourceCache[key] = src; | ||||
|         return src; | ||||
| 
 | ||||
|         IdbLocalStorage._sourceCache[key] = src | ||||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     public static SetDirectly(key: string, value) { | ||||
|  |  | |||
|  | @ -1,51 +1,47 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| /** | ||||
|  * Fetches data from random data sources, used in the metatagging | ||||
|  */ | ||||
| export default class LiveQueryHandler { | ||||
| 
 | ||||
|     private static neededShorthands = {} // url -> (shorthand:paths)[]
 | ||||
| 
 | ||||
|     public static FetchLiveData(url: string, shorthands: string[]): UIEventSource<any /* string -> string */> { | ||||
| 
 | ||||
|     public static FetchLiveData( | ||||
|         url: string, | ||||
|         shorthands: string[] | ||||
|     ): UIEventSource<any /* string -> string */> { | ||||
|         const shorthandsSet: string[] = LiveQueryHandler.neededShorthands[url] ?? [] | ||||
| 
 | ||||
|         for (const shorthand of shorthands) { | ||||
|             if (shorthandsSet.indexOf(shorthand) < 0) { | ||||
|                 shorthandsSet.push(shorthand); | ||||
|                 shorthandsSet.push(shorthand) | ||||
|             } | ||||
|         } | ||||
|         LiveQueryHandler.neededShorthands[url] = shorthandsSet; | ||||
| 
 | ||||
|         LiveQueryHandler.neededShorthands[url] = shorthandsSet | ||||
| 
 | ||||
|         if (LiveQueryHandler[url] === undefined) { | ||||
|             const source = new UIEventSource({}); | ||||
|             LiveQueryHandler[url] = source; | ||||
|             const source = new UIEventSource({}) | ||||
|             LiveQueryHandler[url] = source | ||||
| 
 | ||||
|             console.log("Fetching live data from a third-party (unknown) API:", url) | ||||
|             Utils.downloadJson(url).then(data => { | ||||
|             Utils.downloadJson(url).then((data) => { | ||||
|                 for (const shorthandDescription of shorthandsSet) { | ||||
| 
 | ||||
|                     const descr = shorthandDescription.trim().split(":"); | ||||
|                     const shorthand = descr[0]; | ||||
|                     const path = descr[1]; | ||||
|                     const parts = path.split("."); | ||||
|                     let trail = data; | ||||
|                     const descr = shorthandDescription.trim().split(":") | ||||
|                     const shorthand = descr[0] | ||||
|                     const path = descr[1] | ||||
|                     const parts = path.split(".") | ||||
|                     let trail = data | ||||
|                     for (const part of parts) { | ||||
|                         if (trail !== undefined) { | ||||
|                             trail = trail[part]; | ||||
|                             trail = trail[part] | ||||
|                         } | ||||
|                     } | ||||
|                     source.data[shorthand] = trail; | ||||
|                     source.data[shorthand] = trail | ||||
|                 } | ||||
|                 source.ping(); | ||||
| 
 | ||||
|                 source.ping() | ||||
|             }) | ||||
| 
 | ||||
|         } | ||||
|         return LiveQueryHandler[url]; | ||||
|         return LiveQueryHandler[url] | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,13 +1,12 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| 
 | ||||
| /** | ||||
|  * UIEventsource-wrapper around localStorage | ||||
|  */ | ||||
| export class LocalStorageSource { | ||||
| 
 | ||||
|     static GetParsed<T>(key: string, defaultValue: T): UIEventSource<T> { | ||||
|         return LocalStorageSource.Get(key).sync( | ||||
|             str => { | ||||
|             (str) => { | ||||
|                 if (str === undefined) { | ||||
|                     return defaultValue | ||||
|                 } | ||||
|  | @ -16,29 +15,29 @@ export class LocalStorageSource { | |||
|                 } catch { | ||||
|                     return defaultValue | ||||
|                 } | ||||
|             }, [], | ||||
|             value => JSON.stringify(value) | ||||
|             }, | ||||
|             [], | ||||
|             (value) => JSON.stringify(value) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     static Get(key: string, defaultValue: string = undefined): UIEventSource<string> { | ||||
|         try { | ||||
|             const saved = localStorage.getItem(key); | ||||
|             const source = new UIEventSource<string>(saved ?? defaultValue, "localstorage:" + key); | ||||
|             const saved = localStorage.getItem(key) | ||||
|             const source = new UIEventSource<string>(saved ?? defaultValue, "localstorage:" + key) | ||||
| 
 | ||||
|             source.addCallback((data) => { | ||||
|                 try { | ||||
|                     localStorage.setItem(key, data); | ||||
|                     localStorage.setItem(key, data) | ||||
|                 } catch (e) { | ||||
|                     // Probably exceeded the quota with this item!
 | ||||
|                     // Lets nuke everything
 | ||||
|                     localStorage.clear() | ||||
|                 } | ||||
| 
 | ||||
|             }); | ||||
|             return source; | ||||
|             }) | ||||
|             return source | ||||
|         } catch (e) { | ||||
|             return new UIEventSource<string>(defaultValue); | ||||
|             return new UIEventSource<string>(defaultValue) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,31 +1,31 @@ | |||
| import * as mangrove from 'mangrove-reviews' | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Review} from "./Review"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import * as mangrove from "mangrove-reviews" | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import { Review } from "./Review" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| export class MangroveIdentity { | ||||
|     public keypair: any = undefined; | ||||
|     public readonly kid: UIEventSource<string> = new UIEventSource<string>(undefined); | ||||
|     private readonly _mangroveIdentity: UIEventSource<string>; | ||||
|     public keypair: any = undefined | ||||
|     public readonly kid: UIEventSource<string> = new UIEventSource<string>(undefined) | ||||
|     private readonly _mangroveIdentity: UIEventSource<string> | ||||
| 
 | ||||
|     constructor(mangroveIdentity: UIEventSource<string>) { | ||||
|         const self = this; | ||||
|         this._mangroveIdentity = mangroveIdentity; | ||||
|         mangroveIdentity.addCallbackAndRunD(str => { | ||||
|         const self = this | ||||
|         this._mangroveIdentity = mangroveIdentity | ||||
|         mangroveIdentity.addCallbackAndRunD((str) => { | ||||
|             if (str === "") { | ||||
|                 return; | ||||
|                 return | ||||
|             } | ||||
|             mangrove.jwkToKeypair(JSON.parse(str)).then(keypair => { | ||||
|                 self.keypair = keypair; | ||||
|                 mangrove.publicToPem(keypair.publicKey).then(pem => { | ||||
|             mangrove.jwkToKeypair(JSON.parse(str)).then((keypair) => { | ||||
|                 self.keypair = keypair | ||||
|                 mangrove.publicToPem(keypair.publicKey).then((pem) => { | ||||
|                     console.log("Identity loaded") | ||||
|                     self.kid.setData(pem); | ||||
|                     self.kid.setData(pem) | ||||
|                 }) | ||||
|             }) | ||||
|         }) | ||||
|         try { | ||||
|             if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") { | ||||
|                 this.CreateIdentity(); | ||||
|                 this.CreateIdentity() | ||||
|             } | ||||
|         } catch (e) { | ||||
|             console.error("Could not create identity: ", e) | ||||
|  | @ -41,58 +41,62 @@ export class MangroveIdentity { | |||
|         if ("" !== (this._mangroveIdentity.data ?? "")) { | ||||
|             throw "Identity already defined - not creating a new one" | ||||
|         } | ||||
|         const self = this; | ||||
|         mangrove.generateKeypair().then( | ||||
|             keypair => { | ||||
|                 self.keypair = keypair; | ||||
|                 mangrove.keypairToJwk(keypair).then(jwk => { | ||||
|                     self._mangroveIdentity.setData(JSON.stringify(jwk)); | ||||
|         const self = this | ||||
|         mangrove.generateKeypair().then((keypair) => { | ||||
|             self.keypair = keypair | ||||
|             mangrove.keypairToJwk(keypair).then((jwk) => { | ||||
|                 self._mangroveIdentity.setData(JSON.stringify(jwk)) | ||||
|             }) | ||||
|         }) | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export default class MangroveReviews { | ||||
|     private static _reviewsCache = {}; | ||||
|     private static didWarn = false; | ||||
|     private readonly _lon: number; | ||||
|     private readonly _lat: number; | ||||
|     private readonly _name: string; | ||||
|     private readonly _reviews: UIEventSource<Review[]> = new UIEventSource<Review[]>([]); | ||||
|     private _dryRun: boolean; | ||||
|     private _mangroveIdentity: MangroveIdentity; | ||||
|     private _lastUpdate: Date = undefined; | ||||
|     private static _reviewsCache = {} | ||||
|     private static didWarn = false | ||||
|     private readonly _lon: number | ||||
|     private readonly _lat: number | ||||
|     private readonly _name: string | ||||
|     private readonly _reviews: UIEventSource<Review[]> = new UIEventSource<Review[]>([]) | ||||
|     private _dryRun: boolean | ||||
|     private _mangroveIdentity: MangroveIdentity | ||||
|     private _lastUpdate: Date = undefined | ||||
| 
 | ||||
|     private constructor(lon: number, lat: number, name: string, | ||||
|     private constructor( | ||||
|         lon: number, | ||||
|         lat: number, | ||||
|         name: string, | ||||
|         identity: MangroveIdentity, | ||||
|                         dryRun?: boolean) { | ||||
| 
 | ||||
|         this._lon = lon; | ||||
|         this._lat = lat; | ||||
|         this._name = name; | ||||
|         this._mangroveIdentity = identity; | ||||
|         this._dryRun = dryRun; | ||||
|         dryRun?: boolean | ||||
|     ) { | ||||
|         this._lon = lon | ||||
|         this._lat = lat | ||||
|         this._name = name | ||||
|         this._mangroveIdentity = identity | ||||
|         this._dryRun = dryRun | ||||
|         if (dryRun && !MangroveReviews.didWarn) { | ||||
|             MangroveReviews.didWarn = true; | ||||
|             MangroveReviews.didWarn = true | ||||
|             console.warn("Mangrove reviews will _not_ be saved as dryrun is specified") | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public static Get(lon: number, lat: number, name: string, | ||||
|     public static Get( | ||||
|         lon: number, | ||||
|         lat: number, | ||||
|         name: string, | ||||
|         identity: MangroveIdentity, | ||||
|                       dryRun?: boolean) { | ||||
|         const newReviews = new MangroveReviews(lon, lat, name, identity, dryRun); | ||||
|         dryRun?: boolean | ||||
|     ) { | ||||
|         const newReviews = new MangroveReviews(lon, lat, name, identity, dryRun) | ||||
| 
 | ||||
|         const uri = newReviews.GetSubjectUri(); | ||||
|         const cached = MangroveReviews._reviewsCache[uri]; | ||||
|         const uri = newReviews.GetSubjectUri() | ||||
|         const cached = MangroveReviews._reviewsCache[uri] | ||||
|         if (cached !== undefined) { | ||||
|             return cached; | ||||
|             return cached | ||||
|         } | ||||
|         MangroveReviews._reviewsCache[uri] = newReviews; | ||||
|         MangroveReviews._reviewsCache[uri] = newReviews | ||||
| 
 | ||||
|         return newReviews; | ||||
|         return newReviews | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -100,63 +104,64 @@ export default class MangroveReviews { | |||
|      * @constructor | ||||
|      */ | ||||
|     public GetSubjectUri() { | ||||
|         let uri = `geo:${this._lat},${this._lon}?u=50`; | ||||
|         let uri = `geo:${this._lat},${this._lon}?u=50` | ||||
|         if (this._name !== undefined && this._name !== null) { | ||||
|             uri += "&q=" + this._name; | ||||
|             uri += "&q=" + this._name | ||||
|         } | ||||
|         return uri; | ||||
|         return uri | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Gives a UIEVentsource with all reviews. | ||||
|      * Note: rating is between 1 and 100 | ||||
|      */ | ||||
|     public GetReviews(): UIEventSource<Review[]> { | ||||
| 
 | ||||
|         if (this._lastUpdate !== undefined && this._reviews.data !== undefined && | ||||
|             (new Date().getTime() - this._lastUpdate.getTime()) < 15000 | ||||
|         if ( | ||||
|             this._lastUpdate !== undefined && | ||||
|             this._reviews.data !== undefined && | ||||
|             new Date().getTime() - this._lastUpdate.getTime() < 15000 | ||||
|         ) { | ||||
|             // Last update was pretty recent
 | ||||
|             return this._reviews; | ||||
|             return this._reviews | ||||
|         } | ||||
|         this._lastUpdate = new Date(); | ||||
|         this._lastUpdate = new Date() | ||||
| 
 | ||||
|         const self = this; | ||||
|         mangrove.getReviews({sub: this.GetSubjectUri()}).then( | ||||
|             (data) => { | ||||
|                 const reviews = []; | ||||
|                 const reviewsByUser = []; | ||||
|         const self = this | ||||
|         mangrove.getReviews({ sub: this.GetSubjectUri() }).then((data) => { | ||||
|             const reviews = [] | ||||
|             const reviewsByUser = [] | ||||
|             for (const review of data.reviews) { | ||||
|                     const r = review.payload; | ||||
|                 const r = review.payload | ||||
| 
 | ||||
| 
 | ||||
|                     console.log("PublicKey is ", self._mangroveIdentity.kid.data, "reviews.kid is", review.kid); | ||||
|                     const byUser = self._mangroveIdentity.kid.map(data => data === review.signature); | ||||
|                 console.log( | ||||
|                     "PublicKey is ", | ||||
|                     self._mangroveIdentity.kid.data, | ||||
|                     "reviews.kid is", | ||||
|                     review.kid | ||||
|                 ) | ||||
|                 const byUser = self._mangroveIdentity.kid.map((data) => data === review.signature) | ||||
|                 const rev: Review = { | ||||
|                     made_by_user: byUser, | ||||
|                     date: new Date(r.iat * 1000), | ||||
|                     comment: r.opinion, | ||||
|                     author: r.metadata.nickname, | ||||
|                     affiliated: r.metadata.is_affiliated, | ||||
|                         rating: r.rating // percentage points
 | ||||
|                     }; | ||||
|                     rating: r.rating, // percentage points
 | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                     (rev.made_by_user ? reviewsByUser : reviews).push(rev); | ||||
|                 ;(rev.made_by_user ? reviewsByUser : reviews).push(rev) | ||||
|             } | ||||
|             self._reviews.setData(reviewsByUser.concat(reviews)) | ||||
|             } | ||||
|         ); | ||||
|         return this._reviews; | ||||
|         }) | ||||
|         return this._reviews | ||||
|     } | ||||
| 
 | ||||
|     AddReview(r: Review, callback?: (() => void)) { | ||||
| 
 | ||||
| 
 | ||||
|         callback = callback ?? (() => { | ||||
|             return undefined; | ||||
|         }); | ||||
|     AddReview(r: Review, callback?: () => void) { | ||||
|         callback = | ||||
|             callback ?? | ||||
|             (() => { | ||||
|                 return undefined | ||||
|             }) | ||||
| 
 | ||||
|         const payload = { | ||||
|             sub: this.GetSubjectUri(), | ||||
|  | @ -164,35 +169,29 @@ export default class MangroveReviews { | |||
|             opinion: r.comment, | ||||
|             metadata: { | ||||
|                 nickname: r.author, | ||||
|             }, | ||||
|         } | ||||
|         }; | ||||
|         if (r.affiliated) { | ||||
|             // @ts-ignore
 | ||||
|             payload.metadata.is_affiliated = true; | ||||
|             payload.metadata.is_affiliated = true | ||||
|         } | ||||
|         if (this._dryRun) { | ||||
|             console.warn("DRYRUNNING mangrove reviews: ", payload); | ||||
|             console.warn("DRYRUNNING mangrove reviews: ", payload) | ||||
|             if (callback) { | ||||
|                 if (callback) { | ||||
|                     callback(); | ||||
|                     callback() | ||||
|                 } | ||||
|                 this._reviews.data.push(r); | ||||
|                 this._reviews.ping(); | ||||
| 
 | ||||
|                 this._reviews.data.push(r) | ||||
|                 this._reviews.ping() | ||||
|             } | ||||
|         } else { | ||||
|             mangrove.signAndSubmitReview(this._mangroveIdentity.keypair, payload).then(() => { | ||||
|                 if (callback) { | ||||
|                     callback(); | ||||
|                     callback() | ||||
|                 } | ||||
|                 this._reviews.data.push(r); | ||||
|                 this._reviews.ping(); | ||||
| 
 | ||||
|                 this._reviews.data.push(r) | ||||
|                 this._reviews.ping() | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -1,42 +1,52 @@ | |||
| /** | ||||
|  * Wraps the query parameters into UIEventSources | ||||
|  */ | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Hash from "./Hash"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import Hash from "./Hash" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| export class QueryParameters { | ||||
| 
 | ||||
|     static defaults = {} | ||||
|     static documentation: Map<string, string> = new Map<string, string>() | ||||
|     private static order: string [] = ["layout", "test", "z", "lat", "lon"]; | ||||
|     private static order: string[] = ["layout", "test", "z", "lat", "lon"] | ||||
|     private static _wasInitialized: Set<string> = new Set() | ||||
|     private static knownSources = {}; | ||||
|     private static initialized = false; | ||||
|     private static knownSources = {} | ||||
|     private static initialized = false | ||||
| 
 | ||||
|     public static GetQueryParameter(key: string, deflt: string, documentation?: string): UIEventSource<string> { | ||||
|     public static GetQueryParameter( | ||||
|         key: string, | ||||
|         deflt: string, | ||||
|         documentation?: string | ||||
|     ): UIEventSource<string> { | ||||
|         if (!this.initialized) { | ||||
|             this.init(); | ||||
|             this.init() | ||||
|         } | ||||
|         QueryParameters.documentation.set(key, documentation); | ||||
|         QueryParameters.documentation.set(key, documentation) | ||||
|         if (deflt !== undefined) { | ||||
|             QueryParameters.defaults[key] = deflt; | ||||
|             QueryParameters.defaults[key] = deflt | ||||
|         } | ||||
|         if (QueryParameters.knownSources[key] !== undefined) { | ||||
|             return QueryParameters.knownSources[key]; | ||||
|             return QueryParameters.knownSources[key] | ||||
|         } | ||||
|         QueryParameters.addOrder(key); | ||||
|         const source = new UIEventSource<string>(deflt, "&" + key); | ||||
|         QueryParameters.knownSources[key] = source; | ||||
|         QueryParameters.addOrder(key) | ||||
|         const source = new UIEventSource<string>(deflt, "&" + key) | ||||
|         QueryParameters.knownSources[key] = source | ||||
|         source.addCallback(() => QueryParameters.Serialize()) | ||||
|         return source; | ||||
|         return source | ||||
|     } | ||||
| 
 | ||||
|     public static GetBooleanQueryParameter(key: string, deflt: boolean, documentation?: string): UIEventSource<boolean> { | ||||
|         return QueryParameters.GetQueryParameter(key, ""+ deflt, documentation).sync(str => str === "true", [], b => "" + b) | ||||
|     public static GetBooleanQueryParameter( | ||||
|         key: string, | ||||
|         deflt: boolean, | ||||
|         documentation?: string | ||||
|     ): UIEventSource<boolean> { | ||||
|         return QueryParameters.GetQueryParameter(key, "" + deflt, documentation).sync( | ||||
|             (str) => str === "true", | ||||
|             [], | ||||
|             (b) => "" + b | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static wasInitialized(key: string): boolean { | ||||
|         return QueryParameters._wasInitialized.has(key) | ||||
|     } | ||||
|  | @ -48,53 +58,54 @@ export class QueryParameters { | |||
|     } | ||||
| 
 | ||||
|     private static init() { | ||||
| 
 | ||||
|         if (this.initialized) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
|         this.initialized = true; | ||||
|         this.initialized = true | ||||
| 
 | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return; | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (window?.location?.search) { | ||||
|             const params = window.location.search.substr(1).split("&"); | ||||
|             const params = window.location.search.substr(1).split("&") | ||||
|             for (const param of params) { | ||||
|                 const kv = param.split("="); | ||||
|                 const key = decodeURIComponent(kv[0]); | ||||
|                 const kv = param.split("=") | ||||
|                 const key = decodeURIComponent(kv[0]) | ||||
|                 QueryParameters.addOrder(key) | ||||
|                 QueryParameters._wasInitialized.add(key) | ||||
|                 const v = decodeURIComponent(kv[1]); | ||||
|                 const source = new UIEventSource<string>(v); | ||||
|                 const v = decodeURIComponent(kv[1]) | ||||
|                 const source = new UIEventSource<string>(v) | ||||
|                 source.addCallback(() => QueryParameters.Serialize()) | ||||
|                 QueryParameters.knownSources[key] = source; | ||||
|                 QueryParameters.knownSources[key] = source | ||||
|             } | ||||
|         } | ||||
|          | ||||
|     } | ||||
| 
 | ||||
|     private static Serialize() { | ||||
|         const parts = [] | ||||
|         for (const key of QueryParameters.order) { | ||||
|             if (QueryParameters.knownSources[key]?.data === undefined) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (QueryParameters.knownSources[key].data === "undefined") { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (QueryParameters.knownSources[key].data === QueryParameters.defaults[key]) { | ||||
|                 continue; | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data)) | ||||
|             parts.push( | ||||
|                 encodeURIComponent(key) + | ||||
|                     "=" + | ||||
|                     encodeURIComponent(QueryParameters.knownSources[key].data) | ||||
|             ) | ||||
|         } | ||||
|         if(!Utils.runningFromConsole){ | ||||
|         if (!Utils.runningFromConsole) { | ||||
|             // Don't pollute the history every time a parameter changes
 | ||||
|             history.replaceState(null, "", "?" + parts.join("&") + Hash.Current()); | ||||
|             history.replaceState(null, "", "?" + parts.join("&") + Hash.Current()) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -1,11 +1,11 @@ | |||
| import {Store} from "../UIEventSource"; | ||||
| import { Store } from "../UIEventSource" | ||||
| 
 | ||||
| export interface Review { | ||||
|     comment?: string, | ||||
|     author: string, | ||||
|     date: Date, | ||||
|     rating: number, | ||||
|     affiliated: boolean, | ||||
|     comment?: string | ||||
|     author: string | ||||
|     date: Date | ||||
|     rating: number | ||||
|     affiliated: boolean | ||||
|     /** | ||||
|      * True if the current logged in user is the creator of this comment | ||||
|      */ | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import {Utils} from "../../Utils"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import { Utils } from "../../Utils" | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import * as wds from "wikidata-sdk" | ||||
| 
 | ||||
| export class WikidataResponse { | ||||
|  | @ -18,14 +18,12 @@ export class WikidataResponse { | |||
|         wikisites: Map<string, string>, | ||||
|         commons: string | ||||
|     ) { | ||||
| 
 | ||||
|         this.id = id | ||||
|         this.labels = labels | ||||
|         this.descriptions = descriptions | ||||
|         this.claims = claims | ||||
|         this.wikisites = wikisites | ||||
|         this.commons = commons | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public static fromJson(entity: any): WikidataResponse { | ||||
|  | @ -41,7 +39,7 @@ export class WikidataResponse { | |||
|             descr.set(labelName, entity.descriptions[labelName].value) | ||||
|         } | ||||
| 
 | ||||
|         const sitelinks = new Map<string, string>(); | ||||
|         const sitelinks = new Map<string, string>() | ||||
|         for (const labelName in entity.sitelinks) { | ||||
|             // labelName is `${language}wiki`
 | ||||
|             const language = labelName.substring(0, labelName.length - 4) | ||||
|  | @ -51,28 +49,19 @@ export class WikidataResponse { | |||
| 
 | ||||
|         const commons = sitelinks.get("commons") | ||||
|         sitelinks.delete("commons") | ||||
|         const claims = WikidataResponse.extractClaims(entity.claims); | ||||
|         return new WikidataResponse( | ||||
|             entity.id, | ||||
|             labels, | ||||
|             descr, | ||||
|             claims, | ||||
|             sitelinks, | ||||
|             commons | ||||
|         ) | ||||
| 
 | ||||
|         const claims = WikidataResponse.extractClaims(entity.claims) | ||||
|         return new WikidataResponse(entity.id, labels, descr, claims, sitelinks, commons) | ||||
|     } | ||||
| 
 | ||||
|     static extractClaims(claimsJson: any): Map<string, Set<string>> { | ||||
| 
 | ||||
|         const simplified = wds.simplify.claims(claimsJson, { | ||||
|             timeConverter: 'simple-day' | ||||
|             timeConverter: "simple-day", | ||||
|         }) | ||||
| 
 | ||||
|         const claims = new Map<string, Set<string>>(); | ||||
|         const claims = new Map<string, Set<string>>() | ||||
|         for (const claimId in simplified) { | ||||
|             const claimsList: any[] = simplified[claimId] | ||||
|             claims.set(claimId, new Set(claimsList)); | ||||
|             claims.set(claimId, new Set(claimsList)) | ||||
|         } | ||||
|         return claims | ||||
|     } | ||||
|  | @ -84,7 +73,6 @@ export class WikidataLexeme { | |||
|     senses: Map<string, string> | ||||
|     claims: Map<string, Set<string>> | ||||
| 
 | ||||
| 
 | ||||
|     constructor(json) { | ||||
|         this.id = json.id | ||||
|         this.claims = WikidataResponse.extractClaims(json.claims) | ||||
|  | @ -117,36 +105,40 @@ export class WikidataLexeme { | |||
|             this.claims, | ||||
|             new Map(), | ||||
|             undefined | ||||
|         ); | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export interface WikidataSearchoptions { | ||||
|     lang?: "en" | string, | ||||
|     lang?: "en" | string | ||||
|     maxCount?: 20 | number | ||||
| } | ||||
| 
 | ||||
| export interface WikidataAdvancedSearchoptions extends WikidataSearchoptions { | ||||
|     instanceOf?: number[]; | ||||
|     instanceOf?: number[] | ||||
|     notInstanceOf?: number[] | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Utility functions around wikidata | ||||
|  */ | ||||
| export default class Wikidata { | ||||
| 
 | ||||
|     private static readonly _identifierPrefixes = ["Q", "L"].map(str => str.toLowerCase()) | ||||
|     private static readonly _prefixesToRemove = ["https://www.wikidata.org/wiki/Lexeme:",  | ||||
|     private static readonly _identifierPrefixes = ["Q", "L"].map((str) => str.toLowerCase()) | ||||
|     private static readonly _prefixesToRemove = [ | ||||
|         "https://www.wikidata.org/wiki/Lexeme:", | ||||
|         "https://www.wikidata.org/wiki/", | ||||
|         "http://www.wikidata.org/entity/", | ||||
|         "Lexeme:"].map(str => str.toLowerCase()) | ||||
|         "Lexeme:", | ||||
|     ].map((str) => str.toLowerCase()) | ||||
| 
 | ||||
|     private static readonly _cache = new Map< | ||||
|         string, | ||||
|         UIEventSource<{ success: WikidataResponse } | { error: any }> | ||||
|     >() | ||||
| 
 | ||||
|     private static readonly _cache = new Map<string, UIEventSource<{ success: WikidataResponse } | { error: any }>>() | ||||
| 
 | ||||
|     public static LoadWikidataEntry(value: string | number): UIEventSource<{ success: WikidataResponse } | { error: any }> { | ||||
|     public static LoadWikidataEntry( | ||||
|         value: string | number | ||||
|     ): UIEventSource<{ success: WikidataResponse } | { error: any }> { | ||||
|         const key = this.ExtractKey(value) | ||||
|         const cached = Wikidata._cache.get(key) | ||||
|         if (cached !== undefined) { | ||||
|  | @ -154,27 +146,31 @@ export default class Wikidata { | |||
|         } | ||||
|         const src = UIEventSource.FromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key)) | ||||
|         Wikidata._cache.set(key, src) | ||||
|         return src; | ||||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a search text, searches for the relevant wikidata entries, excluding pages "outside of the main tree", e.g. disambiguation pages. | ||||
|      * Optionally, an 'instance of' can be given to limit the scope, e.g. instanceOf:5 (humans) will only search for humans | ||||
|      */ | ||||
|     public static async searchAdvanced(text: string, options: WikidataAdvancedSearchoptions): Promise<{ | ||||
|         id: string, | ||||
|         relevance?: number, | ||||
|         label: string, | ||||
|     public static async searchAdvanced( | ||||
|         text: string, | ||||
|         options: WikidataAdvancedSearchoptions | ||||
|     ): Promise< | ||||
|         { | ||||
|             id: string | ||||
|             relevance?: number | ||||
|             label: string | ||||
|             description?: string | ||||
|     }[]> { | ||||
|         }[] | ||||
|     > { | ||||
|         let instanceOf = "" | ||||
|         if (options?.instanceOf !== undefined && options.instanceOf.length > 0) { | ||||
|            const phrases = options.instanceOf.map(q => `{ ?item wdt:P31/wdt:P279* wd:Q${q}. }`) | ||||
|             instanceOf = "{"+ phrases.join(" UNION ") + "}" | ||||
|             const phrases = options.instanceOf.map((q) => `{ ?item wdt:P31/wdt:P279* wd:Q${q}. }`) | ||||
|             instanceOf = "{" + phrases.join(" UNION ") + "}" | ||||
|         } | ||||
|         const forbidden = (options?.notInstanceOf ?? []) | ||||
|             .concat([17379835]) // blacklist 'wikimedia pages outside of the main knowledge tree', e.g. disambiguation pages
 | ||||
|         const minusPhrases = forbidden.map(q => `MINUS {?item wdt:P31/wdt:P279* wd:Q${q} .}`) | ||||
|         const forbidden = (options?.notInstanceOf ?? []).concat([17379835]) // blacklist 'wikimedia pages outside of the main knowledge tree', e.g. disambiguation pages
 | ||||
|         const minusPhrases = forbidden.map((q) => `MINUS {?item wdt:P31/wdt:P279* wd:Q${q} .}`) | ||||
|         const sparql = `SELECT * WHERE {
 | ||||
|             SERVICE wikibase:mwapi { | ||||
|                 bd:serviceParam wikibase:api "EntitySearch" . | ||||
|  | @ -183,7 +179,11 @@ export default class Wikidata { | |||
|                 bd:serviceParam mwapi:language "${options.lang}" . | ||||
|                 ?item wikibase:apiOutputItem mwapi:item . | ||||
|                 ?num wikibase:apiOrdinal true . | ||||
|                 bd:serviceParam wikibase:limit ${Math.round((options.maxCount ?? 20) * 1.5) /*Some padding for disambiguation pages */} . | ||||
|                 bd:serviceParam wikibase:limit ${ | ||||
|                     Math.round( | ||||
|                         (options.maxCount ?? 20) * 1.5 | ||||
|                     ) /*Some padding for disambiguation pages */ | ||||
|                 } . | ||||
|                 ?label wikibase:apiOutput mwapi:label . | ||||
|                 ?description wikibase:apiOutput "@description" . | ||||
|             }  | ||||
|  | @ -195,11 +195,11 @@ export default class Wikidata { | |||
|         const result = await Utils.downloadJson(url) | ||||
|         /*The full uri of the wikidata-item*/ | ||||
| 
 | ||||
|         return result.results.bindings.map(({item, label, description, num}) => ({ | ||||
|         return result.results.bindings.map(({ item, label, description, num }) => ({ | ||||
|             relevance: num?.value, | ||||
|             id: item?.value, | ||||
|             label: label?.value, | ||||
|             description: description?.value | ||||
|             description: description?.value, | ||||
|         })) | ||||
|     } | ||||
| 
 | ||||
|  | @ -207,47 +207,47 @@ export default class Wikidata { | |||
|         search: string, | ||||
|         options?: WikidataSearchoptions, | ||||
|         page = 1 | ||||
|     ): Promise<{ | ||||
|         id: string, | ||||
|         label: string, | ||||
|     ): Promise< | ||||
|         { | ||||
|             id: string | ||||
|             label: string | ||||
|             description: string | ||||
|     }[]> { | ||||
|         }[] | ||||
|     > { | ||||
|         const maxCount = options?.maxCount ?? 20 | ||||
|         let pageCount = Math.min(maxCount, 50) | ||||
|         const start = page * pageCount - pageCount; | ||||
|         const lang = (options?.lang ?? "en") | ||||
|         const start = page * pageCount - pageCount | ||||
|         const lang = options?.lang ?? "en" | ||||
|         const url = | ||||
|             "https://www.wikidata.org/w/api.php?action=wbsearchentities&search=" + | ||||
|             search + | ||||
|             "&language=" + | ||||
|             lang + | ||||
|             "&limit=" + pageCount + "&continue=" + | ||||
|             "&limit=" + | ||||
|             pageCount + | ||||
|             "&continue=" + | ||||
|             start + | ||||
|             "&format=json&uselang=" + | ||||
|             lang + | ||||
|             "&type=item&origin=*" + | ||||
|             "&props=";// props= removes some unused values in the result
 | ||||
|             "&props=" // props= removes some unused values in the result
 | ||||
|         const response = await Utils.downloadJsonCached(url, 10000) | ||||
| 
 | ||||
|         const result: any[] = response.search | ||||
| 
 | ||||
|         if (result.length < pageCount) { | ||||
|             // No next page
 | ||||
|             return result; | ||||
|             return result | ||||
|         } | ||||
|         if (result.length < maxCount) { | ||||
|             const newOptions = {...options} | ||||
|             const newOptions = { ...options } | ||||
|             newOptions.maxCount = maxCount - result.length | ||||
|             result.push(...await Wikidata.search(search, | ||||
|                 newOptions, | ||||
|                 page + 1 | ||||
|             )) | ||||
|             result.push(...(await Wikidata.search(search, newOptions, page + 1))) | ||||
|         } | ||||
| 
 | ||||
|         return result; | ||||
|         return result | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static async searchAndFetch( | ||||
|         search: string, | ||||
|         options?: WikidataAdvancedSearchoptions | ||||
|  | @ -255,16 +255,17 @@ export default class Wikidata { | |||
|         // We provide some padding to filter away invalid values
 | ||||
|         const searchResults = await Wikidata.searchAdvanced(search, options) | ||||
|         const maybeResponses = await Promise.all( | ||||
|             searchResults.map(async r => { | ||||
|             searchResults.map(async (r) => { | ||||
|                 try { | ||||
|                     console.log("Loading ", r.id) | ||||
|                     return await Wikidata.LoadWikidataEntry(r.id).AsPromise() | ||||
|                 } catch (e) { | ||||
|                     console.error(e) | ||||
|                     return undefined; | ||||
|                     return undefined | ||||
|                 } | ||||
|             })) | ||||
|         return Utils.NoNull(maybeResponses.map(r => <WikidataResponse>r["success"])) | ||||
|             }) | ||||
|         ) | ||||
|         return Utils.NoNull(maybeResponses.map((r) => <WikidataResponse>r["success"])) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -279,7 +280,7 @@ export default class Wikidata { | |||
|         } | ||||
|         if (value === undefined) { | ||||
|             console.error("ExtractKey: value is undefined") | ||||
|             return undefined; | ||||
|             return undefined | ||||
|         } | ||||
|         value = value.trim().toLowerCase() | ||||
| 
 | ||||
|  | @ -296,7 +297,7 @@ export default class Wikidata { | |||
| 
 | ||||
|         for (const identifierPrefix of Wikidata._identifierPrefixes) { | ||||
|             if (value.startsWith(identifierPrefix)) { | ||||
|                 const trimmed = value.substring(identifierPrefix.length); | ||||
|                 const trimmed = value.substring(identifierPrefix.length) | ||||
|                 if (trimmed === "") { | ||||
|                     return undefined | ||||
|                 } | ||||
|  | @ -304,7 +305,7 @@ export default class Wikidata { | |||
|                 if (isNaN(n)) { | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return value.toUpperCase(); | ||||
|                 return value.toUpperCase() | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -312,7 +313,7 @@ export default class Wikidata { | |||
|             return "Q" + value | ||||
|         } | ||||
| 
 | ||||
|         return undefined; | ||||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -326,10 +327,10 @@ export default class Wikidata { | |||
|      * Wikidata.QIdToNumber(123) // => 123
 | ||||
|      */ | ||||
|     public static QIdToNumber(q: string | number): number | undefined { | ||||
|         if(q === undefined || q === null){ | ||||
|         if (q === undefined || q === null) { | ||||
|             return | ||||
|         } | ||||
|         if(typeof q === "number"){ | ||||
|         if (typeof q === "number") { | ||||
|             return q | ||||
|         } | ||||
|         q = q.trim() | ||||
|  | @ -361,12 +362,18 @@ export default class Wikidata { | |||
|      * @param statements | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static async Sparql<T>(keys: string[], statements: string[]):Promise< (T & Record<string, {type: string, value: string}>) []> { | ||||
|         const query = "SELECT "+keys.map(k => k.startsWith("?") ? k : "?"+k).join(" ")+"\n" + | ||||
|     public static async Sparql<T>( | ||||
|         keys: string[], | ||||
|         statements: string[] | ||||
|     ): Promise<(T & Record<string, { type: string; value: string }>)[]> { | ||||
|         const query = | ||||
|             "SELECT " + | ||||
|             keys.map((k) => (k.startsWith("?") ? k : "?" + k)).join(" ") + | ||||
|             "\n" + | ||||
|             "WHERE\n" + | ||||
|             "{\n" + | ||||
|             statements.map(stmt => stmt.endsWith(".") ? stmt : stmt+".").join("\n") + | ||||
|             "  SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE]\". }\n" + | ||||
|             statements.map((stmt) => (stmt.endsWith(".") ? stmt : stmt + ".")).join("\n") + | ||||
|             '  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE]". }\n' + | ||||
|             "}" | ||||
|         const url = wds.sparqlQuery(query) | ||||
|         const result = await Utils.downloadJsonCached(url, 24 * 60 * 60 * 1000) | ||||
|  | @ -384,7 +391,7 @@ export default class Wikidata { | |||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         const url = "https://www.wikidata.org/wiki/Special:EntityData/" + id + ".json"; | ||||
|         const url = "https://www.wikidata.org/wiki/Special:EntityData/" + id + ".json" | ||||
|         const entities = (await Utils.downloadJsonCached(url, 10000)).entities | ||||
|         const firstKey = <string>Array.from(Object.keys(entities))[0] // Roundabout way to fetch the entity; it might have been a redirect
 | ||||
|         const response = entities[firstKey] | ||||
|  | @ -396,5 +403,4 @@ export default class Wikidata { | |||
| 
 | ||||
|         return WikidataResponse.fromJson(response) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {Utils} from "../../Utils"; | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| export default class Wikimedia { | ||||
|     /** | ||||
|  | @ -8,39 +8,47 @@ export default class Wikimedia { | |||
|      * @param maxLoad: the maximum amount of images to return | ||||
|      * @param continueParameter: if the page indicates that more pages should be loaded, this uses a token to continue. Provided by wikimedia | ||||
|      */ | ||||
|     public static async GetCategoryContents(categoryName: string, | ||||
|     public static async GetCategoryContents( | ||||
|         categoryName: string, | ||||
|         maxLoad = 10, | ||||
|                                             continueParameter: string = undefined): Promise<string[]> { | ||||
|         continueParameter: string = undefined | ||||
|     ): Promise<string[]> { | ||||
|         if (categoryName === undefined || categoryName === null || categoryName === "") { | ||||
|             return []; | ||||
|             return [] | ||||
|         } | ||||
|         if (!categoryName.startsWith("Category:")) { | ||||
|             categoryName = "Category:" + categoryName; | ||||
|             categoryName = "Category:" + categoryName | ||||
|         } | ||||
| 
 | ||||
|         let url = "https://commons.wikimedia.org/w/api.php?" + | ||||
|         let url = | ||||
|             "https://commons.wikimedia.org/w/api.php?" + | ||||
|             "action=query&list=categorymembers&format=json&" + | ||||
|             "&origin=*" + | ||||
|             "&cmtitle=" + encodeURIComponent(categoryName); | ||||
|             "&cmtitle=" + | ||||
|             encodeURIComponent(categoryName) | ||||
|         if (continueParameter !== undefined) { | ||||
|             url = `${url}&cmcontinue=${continueParameter}`; | ||||
|             url = `${url}&cmcontinue=${continueParameter}` | ||||
|         } | ||||
|         const response = await Utils.downloadJson(url) | ||||
|         const members = response.query?.categorymembers ?? []; | ||||
|         const imageOverview: string[] = members.map(member => member.title); | ||||
|         const members = response.query?.categorymembers ?? [] | ||||
|         const imageOverview: string[] = members.map((member) => member.title) | ||||
| 
 | ||||
|         if (response.continue === undefined) { | ||||
|             // We are done crawling through the category - no continuation in sight
 | ||||
|             return imageOverview; | ||||
|             return imageOverview | ||||
|         } | ||||
| 
 | ||||
|         if (maxLoad - imageOverview.length <= 0) { | ||||
|             console.debug(`Recursive wikimedia category load stopped for ${categoryName}`) | ||||
|             return imageOverview; | ||||
|             return imageOverview | ||||
|         } | ||||
| 
 | ||||
|         // We do have a continue token - let's load the next page
 | ||||
|         const recursive = await Wikimedia.GetCategoryContents(categoryName, maxLoad - imageOverview.length, response.continue.cmcontinue) | ||||
|         const recursive = await Wikimedia.GetCategoryContents( | ||||
|             categoryName, | ||||
|             maxLoad - imageOverview.length, | ||||
|             response.continue.cmcontinue | ||||
|         ) | ||||
|         imageOverview.push(...recursive) | ||||
|         return imageOverview | ||||
|     } | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue