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 * as known_themes from "../assets/generated/known_layers_and_themes.json" | ||||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | ||||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||||
| import BaseUIElement from "../UI/BaseUIElement"; | import BaseUIElement from "../UI/BaseUIElement" | ||||||
| import Combine from "../UI/Base/Combine"; | import Combine from "../UI/Base/Combine" | ||||||
| import Title from "../UI/Base/Title"; | import Title from "../UI/Base/Title" | ||||||
| import List from "../UI/Base/List"; | import List from "../UI/Base/List" | ||||||
| import DependencyCalculator from "../Models/ThemeConfig/DependencyCalculator"; | import DependencyCalculator from "../Models/ThemeConfig/DependencyCalculator" | ||||||
| import Constants from "../Models/Constants"; | import Constants from "../Models/Constants" | ||||||
| import {Utils} from "../Utils"; | import { Utils } from "../Utils" | ||||||
| import Link from "../UI/Base/Link"; | import Link from "../UI/Base/Link" | ||||||
| import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; | import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" | ||||||
| import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" | ||||||
| 
 | 
 | ||||||
| export class AllKnownLayouts { | export class AllKnownLayouts { | ||||||
|     public static allKnownLayouts: Map<string, LayoutConfig> = AllKnownLayouts.AllLayouts(); |     public static allKnownLayouts: Map<string, LayoutConfig> = AllKnownLayouts.AllLayouts() | ||||||
|     public static layoutsList: LayoutConfig[] = AllKnownLayouts.GenerateOrderedList(AllKnownLayouts.allKnownLayouts); |     public static layoutsList: LayoutConfig[] = AllKnownLayouts.GenerateOrderedList( | ||||||
|  |         AllKnownLayouts.allKnownLayouts | ||||||
|  |     ) | ||||||
|     // Must be below the list...
 |     // 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?: { |     public static AllPublicLayers(options?: { | ||||||
|         includeInlineLayers:true | boolean |         includeInlineLayers: true | boolean | ||||||
|     }) : LayerConfig[] { |     }): LayerConfig[] { | ||||||
|         const allLayers: LayerConfig[] = [] |         const allLayers: LayerConfig[] = [] | ||||||
|         const seendIds = new Set<string>() |         const seendIds = new Set<string>() | ||||||
|         AllKnownLayouts.sharedLayers.forEach((layer, key) => { |         AllKnownLayouts.sharedLayers.forEach((layer, key) => { | ||||||
|  | @ -28,7 +30,7 @@ export class AllKnownLayouts { | ||||||
|             allLayers.push(layer) |             allLayers.push(layer) | ||||||
|         }) |         }) | ||||||
|         if (options?.includeInlineLayers ?? true) { |         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) { |             for (const layout of publicLayouts) { | ||||||
|                 if (layout.hideFromOverview) { |                 if (layout.hideFromOverview) { | ||||||
|                     continue |                     continue | ||||||
|  | @ -40,7 +42,6 @@ export class AllKnownLayouts { | ||||||
|                     seendIds.add(layer.id) |                     seendIds.add(layer.id) | ||||||
|                     allLayers.push(layer) |                     allLayers.push(layer) | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -52,11 +53,14 @@ export class AllKnownLayouts { | ||||||
|      */ |      */ | ||||||
|     public static themesUsingLayer(id: string, publicOnly = true): LayoutConfig[] { |     public static themesUsingLayer(id: string, publicOnly = true): LayoutConfig[] { | ||||||
|         const themes = AllKnownLayouts.layoutsList |         const themes = AllKnownLayouts.layoutsList | ||||||
|             .filter(l => !(publicOnly && l.hideFromOverview) && l.id !== "personal") |             .filter((l) => !(publicOnly && l.hideFromOverview) && l.id !== "personal") | ||||||
|             .map(theme => ({theme, minzoom: theme.layers.find(layer => layer.id === id)?.minzoom})) |             .map((theme) => ({ | ||||||
|             .filter(obj => obj.minzoom !== undefined) |                 theme, | ||||||
|  |                 minzoom: theme.layers.find((layer) => layer.id === id)?.minzoom, | ||||||
|  |             })) | ||||||
|  |             .filter((obj) => obj.minzoom !== undefined) | ||||||
|         themes.sort((th0, th1) => th1.minzoom - th0.minzoom) |         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 |      * @param callback | ||||||
|      * @constructor |      * @constructor | ||||||
|      */ |      */ | ||||||
|     public static GenOverviewsForSingleLayer(callback: (layer: LayerConfig, element: BaseUIElement, inlineSource: string) => void): void { |     public static GenOverviewsForSingleLayer( | ||||||
|         const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()) |         callback: (layer: LayerConfig, element: BaseUIElement, inlineSource: string) => void | ||||||
|             .filter(layer => Constants.priviliged_layers.indexOf(layer.id) < 0) |     ): 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>() |         const builtinLayerIds: Set<string> = new Set<string>() | ||||||
|         allLayers.forEach(l => builtinLayerIds.add(l.id)) |         allLayers.forEach((l) => builtinLayerIds.add(l.id)) | ||||||
|         const inlineLayers = new Map<string, string>(); |         const inlineLayers = new Map<string, string>() | ||||||
| 
 | 
 | ||||||
|         for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) { |         for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) { | ||||||
|             if (layout.hideFromOverview) { |             if (layout.hideFromOverview) { | ||||||
|  | @ -78,7 +85,6 @@ export class AllKnownLayouts { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             for (const layer of layout.layers) { |             for (const layer of layout.layers) { | ||||||
| 
 |  | ||||||
|                 if (Constants.priviliged_layers.indexOf(layer.id) >= 0) { |                 if (Constants.priviliged_layers.indexOf(layer.id) >= 0) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|  | @ -113,7 +119,6 @@ export class AllKnownLayouts { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         // Determine the cross-dependencies
 |         // Determine the cross-dependencies
 | ||||||
|         const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>() |         const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>() | ||||||
| 
 | 
 | ||||||
|  | @ -125,12 +130,14 @@ export class AllKnownLayouts { | ||||||
|                 } |                 } | ||||||
|                 layerIsNeededBy.get(dependency).push(layer.id) |                 layerIsNeededBy.get(dependency).push(layer.id) | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         allLayers.forEach((layer) => { |         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)) |             callback(layer, element, inlineLayers.get(layer.id)) | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  | @ -146,11 +153,12 @@ export class AllKnownLayouts { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()) |         const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()).filter( | ||||||
|             .filter(layer => Constants.priviliged_layers.indexOf(layer.id) < 0) |             (layer) => Constants.priviliged_layers.indexOf(layer.id) < 0 | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         const builtinLayerIds: Set<string> = new Set<string>() |         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[]>() |         const themesPerLayer = new Map<string, string[]>() | ||||||
| 
 | 
 | ||||||
|  | @ -166,7 +174,6 @@ export class AllKnownLayouts { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         // Determine the cross-dependencies
 |         // Determine the cross-dependencies
 | ||||||
|         const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>() |         const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>() | ||||||
| 
 | 
 | ||||||
|  | @ -178,25 +185,32 @@ export class AllKnownLayouts { | ||||||
|                 } |                 } | ||||||
|                 layerIsNeededBy.get(dependency).push(layer.id) |                 layerIsNeededBy.get(dependency).push(layer.id) | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         return new Combine([ |         return new Combine([ | ||||||
|             new Title("Special and other useful layers", 1), |             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.", |             "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 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 |             ...Constants.priviliged_layers | ||||||
|                 .map(id => AllKnownLayouts.sharedLayers.get(id)) |                 .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((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), |             new Title("Normal layers", 1), | ||||||
|             "The following layers are included in MapComplete:", |             "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 { |     public static GenerateDocumentationForTheme(theme: LayoutConfig): BaseUIElement { | ||||||
|  | @ -204,37 +218,42 @@ export class AllKnownLayouts { | ||||||
|             new Title(new Combine([theme.title, "(", theme.id + ")"]), 2), |             new Title(new Combine([theme.title, "(", theme.id + ")"]), 2), | ||||||
|             theme.description, |             theme.description, | ||||||
|             "This theme contains the following layers:", |             "This theme contains the following layers:", | ||||||
|             new List(theme.layers.map(l => l.id)), |             new List(theme.layers.map((l) => l.id)), | ||||||
|             "Available languages:", |             "Available languages:", | ||||||
|             new List(theme.language) |             new List(theme.language), | ||||||
|         ]) |         ]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static getSharedLayers(): Map<string, LayerConfig> { |     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"]) { |         for (const layer of known_themes["layers"]) { | ||||||
|             try { |             try { | ||||||
|                 // @ts-ignore
 |                 // @ts-ignore
 | ||||||
|                 const parsed = new LayerConfig(layer, "shared_layers") |                 const parsed = new LayerConfig(layer, "shared_layers") | ||||||
|                 sharedLayers.set(layer.id, parsed); |                 sharedLayers.set(layer.id, parsed) | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 if (!Utils.runningFromConsole) { |                 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> { |     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"]) { |         for (const layer of known_themes["layers"]) { | ||||||
|                 // @ts-ignore
 |             // @ts-ignore
 | ||||||
|                 sharedLayers.set(layer.id, layer); |             sharedLayers.set(layer.id, layer) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return sharedLayers; |         return sharedLayers | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static GenerateOrderedList(allKnownLayouts: Map<string, LayoutConfig>): LayoutConfig[] { |     private static GenerateOrderedList(allKnownLayouts: Map<string, LayoutConfig>): LayoutConfig[] { | ||||||
|  | @ -242,28 +261,26 @@ export class AllKnownLayouts { | ||||||
|         allKnownLayouts.forEach((layout) => { |         allKnownLayouts.forEach((layout) => { | ||||||
|             list.push(layout) |             list.push(layout) | ||||||
|         }) |         }) | ||||||
|         return list; |         return list | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static AllLayouts(): Map<string, LayoutConfig> { |     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"]) { |         for (const layoutConfigJson of known_themes["themes"]) { | ||||||
|             const layout = new LayoutConfig(<LayoutConfigJson>layoutConfigJson, true) |             const layout = new LayoutConfig(<LayoutConfigJson>layoutConfigJson, true) | ||||||
|             dict.set(layout.id, layout) |             dict.set(layout.id, layout) | ||||||
|             for (let i = 0; i < layout.layers.length; i++) { |             for (let i = 0; i < layout.layers.length; i++) { | ||||||
|                 let layer = layout.layers[i]; |                 let layer = layout.layers[i] | ||||||
|                 if (typeof (layer) === "string") { |                 if (typeof layer === "string") { | ||||||
|                     layer = AllKnownLayouts.sharedLayers.get(layer); |                     layer = AllKnownLayouts.sharedLayers.get(layer) | ||||||
|                     layout.layers[i] = layer |                     layout.layers[i] = layer | ||||||
|                     if (layer === undefined) { |                     if (layer === undefined) { | ||||||
|                         console.log("Defined layers are ", AllKnownLayouts.sharedLayers.keys()) |                         console.log("Defined layers are ", AllKnownLayouts.sharedLayers.keys()) | ||||||
|                         throw `Layer ${layer} was not found or defined - probably a type was made` |                         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 questions from "../assets/tagRenderings/questions.json" | ||||||
| import * as icons from "../assets/tagRenderings/icons.json"; | import * as icons from "../assets/tagRenderings/icons.json" | ||||||
| import {Utils} from "../Utils"; | import { Utils } from "../Utils" | ||||||
| import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; | import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" | ||||||
| import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; | import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson" | ||||||
| import BaseUIElement from "../UI/BaseUIElement"; | import BaseUIElement from "../UI/BaseUIElement" | ||||||
| import Combine from "../UI/Base/Combine"; | import Combine from "../UI/Base/Combine" | ||||||
| import Title from "../UI/Base/Title"; | import Title from "../UI/Base/Title" | ||||||
| import {FixedUiElement} from "../UI/Base/FixedUiElement"; | import { FixedUiElement } from "../UI/Base/FixedUiElement" | ||||||
| import List from "../UI/Base/List"; | import List from "../UI/Base/List" | ||||||
| 
 | 
 | ||||||
| export default class SharedTagRenderings { | export default class SharedTagRenderings { | ||||||
| 
 |     public static SharedTagRendering: Map<string, TagRenderingConfig> = | ||||||
|     public static SharedTagRendering: Map<string, TagRenderingConfig> = SharedTagRenderings.generatedSharedFields(); |         SharedTagRenderings.generatedSharedFields() | ||||||
|     public static SharedTagRenderingJson: Map<string, TagRenderingConfigJson> = SharedTagRenderings.generatedSharedFieldsJsons(); |     public static SharedTagRenderingJson: Map<string, TagRenderingConfigJson> = | ||||||
|     public static SharedIcons: Map<string, TagRenderingConfig> = SharedTagRenderings.generatedSharedFields(true); |         SharedTagRenderings.generatedSharedFieldsJsons() | ||||||
|  |     public static SharedIcons: Map<string, TagRenderingConfig> = | ||||||
|  |         SharedTagRenderings.generatedSharedFields(true) | ||||||
| 
 | 
 | ||||||
|     private static generatedSharedFields(iconsOnly = false): Map<string, TagRenderingConfig> { |     private static generatedSharedFields(iconsOnly = false): Map<string, TagRenderingConfig> { | ||||||
|         const configJsons = SharedTagRenderings.generatedSharedFieldsJsons(iconsOnly) |         const configJsons = SharedTagRenderings.generatedSharedFieldsJsons(iconsOnly) | ||||||
|         const d = new Map<string, TagRenderingConfig>() |         const d = new Map<string, TagRenderingConfig>() | ||||||
|         for (const key of Array.from(configJsons.keys())) { |         for (const key of Array.from(configJsons.keys())) { | ||||||
|             try { |             try { | ||||||
|                 d.set(key, new TagRenderingConfig(configJsons.get(key), `SharedTagRenderings.${key}`)) |                 d.set( | ||||||
|  |                     key, | ||||||
|  |                     new TagRenderingConfig(configJsons.get(key), `SharedTagRenderings.${key}`) | ||||||
|  |                 ) | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 if (!Utils.runningFromConsole) { |                 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 |         return d | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static generatedSharedFieldsJsons(iconsOnly = false): Map<string, TagRenderingConfigJson> { |     private static generatedSharedFieldsJsons( | ||||||
|         const dict = new Map<string, TagRenderingConfigJson>(); |         iconsOnly = false | ||||||
|  |     ): Map<string, TagRenderingConfigJson> { | ||||||
|  |         const dict = new Map<string, TagRenderingConfigJson>() | ||||||
| 
 | 
 | ||||||
|         if (!iconsOnly) { |         if (!iconsOnly) { | ||||||
|             for (const key in questions) { |             for (const key in questions) { | ||||||
|  | @ -53,13 +64,16 @@ export default class SharedTagRenderings { | ||||||
|             if (key === "id") { |             if (key === "id") { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             value.id = value.id ?? key; |             value.id = value.id ?? key | ||||||
|             if(value["builtin"] !== undefined){ |             if (value["builtin"] !== undefined) { | ||||||
|                 if(value["override"] == undefined){ |                 if (value["override"] == undefined) { | ||||||
|                     throw "HUH? Why whould you want to reuse a builtin if one doesn't override? In questions.json/"+key |                     throw ( | ||||||
|  |                         "HUH? Why whould you want to reuse a builtin if one doesn't override? In questions.json/" + | ||||||
|  |                         key | ||||||
|  |                     ) | ||||||
|                 } |                 } | ||||||
|                 if(typeof value["builtin"] !== "string"){ |                 if (typeof value["builtin"] !== "string") { | ||||||
|                     return; |                     return | ||||||
|                 } |                 } | ||||||
|                 // This is a really funny situation: we extend another tagRendering!
 |                 // This is a really funny situation: we extend another tagRendering!
 | ||||||
|                 const parent = Utils.Clone(dict.get(value["builtin"])) |                 const parent = Utils.Clone(dict.get(value["builtin"])) | ||||||
|  | @ -73,36 +87,31 @@ export default class SharedTagRenderings { | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|          |         return dict | ||||||
|         return dict; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     public static HelpText(): BaseUIElement { |     public static HelpText(): BaseUIElement { | ||||||
|         return new Combine([ |         return new Combine([ | ||||||
|             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"), |             ]).SetClass("flex flex-col"), | ||||||
| 
 | 
 | ||||||
|             ... Array.from( SharedTagRenderings.SharedTagRendering.keys()).map(key => { |             ...Array.from(SharedTagRenderings.SharedTagRendering.keys()).map((key) => { | ||||||
|                 const tr = SharedTagRenderings.SharedTagRendering.get(key) |                 const tr = SharedTagRenderings.SharedTagRendering.get(key) | ||||||
|                 let mappings: BaseUIElement = undefined |                 let mappings: BaseUIElement = undefined | ||||||
|                 if(tr.mappings?.length > 0){ |                 if (tr.mappings?.length > 0) { | ||||||
|                     mappings = new List(tr.mappings.map(m => m.then.textFor("en"))) |                     mappings = new List(tr.mappings.map((m) => m.then.textFor("en"))) | ||||||
|                 } |                 } | ||||||
|                 return new Combine([ |                 return new Combine([ | ||||||
|                     new Title(key), |                     new Title(key), | ||||||
|                     tr.render?.textFor("en"), |                     tr.render?.textFor("en"), | ||||||
|                     tr.question?.textFor("en") ?? new FixedUiElement("Read-only tagrendering").SetClass("font-bold"), |                     tr.question?.textFor("en") ?? | ||||||
|                     mappings |                         new FixedUiElement("Read-only tagrendering").SetClass("font-bold"), | ||||||
|  |                     mappings, | ||||||
|                 ]).SetClass("flex flex-col") |                 ]).SetClass("flex flex-col") | ||||||
| 
 |             }), | ||||||
|             }) |  | ||||||
| 
 |  | ||||||
|         ]).SetClass("flex flex-col") |         ]).SetClass("flex flex-col") | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,29 +1,27 @@ | ||||||
| import {existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync} from "fs"; | import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs" | ||||||
| import ScriptUtils from "../../scripts/ScriptUtils"; | import ScriptUtils from "../../scripts/ScriptUtils" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| ScriptUtils.fixUtils() | ScriptUtils.fixUtils() | ||||||
| 
 | 
 | ||||||
| class StatsDownloader { | 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 = ".") { |     constructor(targetDirectory = ".") { | ||||||
|         this._targetDirectory = targetDirectory; |         this._targetDirectory = targetDirectory | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async DownloadStats(startYear = 2020, startMonth = 5, startDay = 1) { |     public async DownloadStats(startYear = 2020, startMonth = 5, startDay = 1) { | ||||||
| 
 |         const today = new Date() | ||||||
|         const today = new Date(); |  | ||||||
|         const currentYear = today.getFullYear() |         const currentYear = today.getFullYear() | ||||||
|         const currentMonth = today.getMonth() + 1 |         const currentMonth = today.getMonth() + 1 | ||||||
|         for (let year = startYear; year <= currentYear; year++) { |         for (let year = startYear; year <= currentYear; year++) { | ||||||
|             for (let month = 1; month <= 12; month++) { |             for (let month = 1; month <= 12; month++) { | ||||||
| 
 |  | ||||||
|                 if (year === startYear && month < startMonth) { |                 if (year === startYear && month < startMonth) { | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (year === currentYear && month > currentMonth) { |                 if (year === currentYear && month > currentMonth) { | ||||||
|  | @ -32,33 +30,40 @@ class StatsDownloader { | ||||||
| 
 | 
 | ||||||
|                 const pathM = `${this._targetDirectory}/stats.${year}-${month}.json` |                 const pathM = `${this._targetDirectory}/stats.${year}-${month}.json` | ||||||
|                 if (existsSync(pathM)) { |                 if (existsSync(pathM)) { | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 const features = [] |                 const features = [] | ||||||
|                 let monthIsFinished = true |                 let monthIsFinished = true | ||||||
|                 const writtenFiles = [] |                 const writtenFiles = [] | ||||||
|                 for (let day = startDay; day <= 31; day++) { |                 for (let day = startDay; day <= 31; day++) { | ||||||
|                      |  | ||||||
|                     if (year === currentYear && month === currentMonth && day === today.getDate()) { |                     if (year === currentYear && month === currentMonth && day === today.getDate()) { | ||||||
|                         monthIsFinished = false |                         monthIsFinished = false | ||||||
|                         break; |                         break | ||||||
|                     } |                     } | ||||||
|                     { |                     { | ||||||
|                         const date = new Date(year, month - 1, day) |                         const date = new Date(year, month - 1, day) | ||||||
|                         if(date.getMonth() != month -1){ |                         if (date.getMonth() != month - 1) { | ||||||
|                             // We did roll over
 |                             // We did roll over
 | ||||||
|                             continue |                             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) |                     writtenFiles.push(path) | ||||||
|                     if (existsSync(path)) { |                     if (existsSync(path)) { | ||||||
|                         let features = JSON.parse(readFileSync(path, "UTF-8")) |                         let features = JSON.parse(readFileSync(path, "UTF-8")) | ||||||
|                         features = features?.features ?? features |                         features = features?.features ?? features | ||||||
|                         console.log(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
 |                         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") |                         console.log( | ||||||
|  |                             "Loaded ", | ||||||
|  |                             path, | ||||||
|  |                             "from disk, got", | ||||||
|  |                             features.length, | ||||||
|  |                             "features now" | ||||||
|  |                         ) | ||||||
|                         continue |                         continue | ||||||
|                     } |                     } | ||||||
|                     let dayFeatures: any[] = undefined |                     let dayFeatures: any[] = undefined | ||||||
|  | @ -66,15 +71,22 @@ class StatsDownloader { | ||||||
|                         dayFeatures = await this.DownloadStatsForDay(year, month, day, path) |                         dayFeatures = await this.DownloadStatsForDay(year, month, day, path) | ||||||
|                     } catch (e) { |                     } catch (e) { | ||||||
|                         console.error(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) |                         dayFeatures = await this.DownloadStatsForDay(year, month, day, path) | ||||||
|                     } |                     } | ||||||
|                     writeFileSync(path, JSON.stringify(dayFeatures)) |                     writeFileSync(path, JSON.stringify(dayFeatures)) | ||||||
|                     features.push(...dayFeatures) |                     features.push(...dayFeatures) | ||||||
| 
 |  | ||||||
|                 } |                 } | ||||||
|                 if(monthIsFinished){ |                 if (monthIsFinished) { | ||||||
|                     writeFileSync(pathM, JSON.stringify({features})) |                     writeFileSync(pathM, JSON.stringify({ features })) | ||||||
|                     for (const writtenFile of writtenFiles) { |                     for (const writtenFile of writtenFiles) { | ||||||
|                         unlinkSync(writtenFile) |                         unlinkSync(writtenFile) | ||||||
|                     } |                     } | ||||||
|  | @ -82,37 +94,49 @@ class StatsDownloader { | ||||||
|             } |             } | ||||||
|             startDay = 1 |             startDay = 1 | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async DownloadStatsForDay(year: number, month: number, day: number, path: string): Promise<any[]> { |     public async DownloadStatsForDay( | ||||||
| 
 |         year: number, | ||||||
|         let page = 1; |         month: number, | ||||||
|  |         day: number, | ||||||
|  |         path: string | ||||||
|  |     ): Promise<any[]> { | ||||||
|  |         let page = 1 | ||||||
|         let allFeatures = [] |         let allFeatures = [] | ||||||
|         let endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1); |         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 endDate = `${endDay.getFullYear()}-${Utils.TwoDigits( | ||||||
|         let url = this.urlTemplate.replace("{start_date}", year + "-" + Utils.TwoDigits(month) + "-" + Utils.TwoDigits(day)) |             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("{end_date}", endDate) | ||||||
|             .replace("{page}", "" + page) |             .replace("{page}", "" + page) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         let headers = { |         let headers = { | ||||||
|             'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0', |             "User-Agent": | ||||||
|             'Accept-Language': 'en-US,en;q=0.5', |                 "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0", | ||||||
|             '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', |             "Accept-Language": "en-US,en;q=0.5", | ||||||
|             'Content-Type': 'application/json', |             Referer: | ||||||
|             'Authorization': 'Token 6e422e2afedb79ef66573982012000281f03dc91', |                 "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", | ||||||
|             'DNT': '1', |             "Content-Type": "application/json", | ||||||
|             'Connection': 'keep-alive', |             Authorization: "Token 6e422e2afedb79ef66573982012000281f03dc91", | ||||||
|             'TE': 'Trailers', |             DNT: "1", | ||||||
|             'Pragma': 'no-cache', |             Connection: "keep-alive", | ||||||
|             'Cache-Control': 'no-cache' |             TE: "Trailers", | ||||||
|  |             Pragma: "no-cache", | ||||||
|  |             "Cache-Control": "no-cache", | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         while (url) { |         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) |             const result = await Utils.downloadJson(url, headers) | ||||||
|             page++; |             page++ | ||||||
|             allFeatures.push(...result.features) |             allFeatures.push(...result.features) | ||||||
|             if (result.features === undefined) { |             if (result.features === undefined) { | ||||||
|                 console.log("ERROR", result) |                 console.log("ERROR", result) | ||||||
|  | @ -120,58 +144,59 @@ class StatsDownloader { | ||||||
|             } |             } | ||||||
|             url = result.next |             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 = Utils.NoNull(allFeatures) | ||||||
|         allFeatures.forEach(f => { |         allFeatures.forEach((f) => { | ||||||
|             f.properties = {...f.properties, ...f.properties.metadata} |             f.properties = { ...f.properties, ...f.properties.metadata } | ||||||
|             delete f.properties.metadata |             delete f.properties.metadata | ||||||
|             f.properties.id = f.id |             f.properties.id = f.id | ||||||
|         }) |         }) | ||||||
|         return allFeatures |         return allFeatures | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| interface ChangeSetData { | interface ChangeSetData { | ||||||
|     "id": number, |     id: number | ||||||
|     "type": "Feature", |     type: "Feature" | ||||||
|     "geometry": { |     geometry: { | ||||||
|         "type": "Polygon", |         type: "Polygon" | ||||||
|         "coordinates": [number, number][][] |         coordinates: [number, number][][] | ||||||
|     }, |     } | ||||||
|     "properties": { |     properties: { | ||||||
|         "check_user": null, |         check_user: null | ||||||
|         "reasons": [], |         reasons: [] | ||||||
|         "tags": [], |         tags: [] | ||||||
|         "features": [], |         features: [] | ||||||
|         "user": string, |         user: string | ||||||
|         "uid": string, |         uid: string | ||||||
|         "editor": string, |         editor: string | ||||||
|         "comment": string, |         comment: string | ||||||
|         "comments_count": number, |         comments_count: number | ||||||
|         "source": string, |         source: string | ||||||
|         "imagery_used": string, |         imagery_used: string | ||||||
|         "date": string, |         date: string | ||||||
|         "reviewed_features": [], |         reviewed_features: [] | ||||||
|         "create": number, |         create: number | ||||||
|         "modify": number, |         modify: number | ||||||
|         "delete": number, |         delete: number | ||||||
|         "area": number, |         area: number | ||||||
|         "is_suspect": boolean, |         is_suspect: boolean | ||||||
|         "harmful": any, |         harmful: any | ||||||
|         "checked": boolean, |         checked: boolean | ||||||
|         "check_date": any, |         check_date: any | ||||||
|         "metadata": { |         metadata: { | ||||||
|             "host": string, |             host: string | ||||||
|             "theme": string, |             theme: string | ||||||
|             "imagery": string, |             imagery: string | ||||||
|             "language": string |             language: string | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| async function main(): Promise<void> { | async function main(): Promise<void> { | ||||||
|     if (!existsSync("graphs")) { |     if (!existsSync("graphs")) { | ||||||
|         mkdirSync("graphs") |         mkdirSync("graphs") | ||||||
|  | @ -181,43 +206,47 @@ async function main(): Promise<void> { | ||||||
|     let year = 2020 |     let year = 2020 | ||||||
|     let month = 5 |     let month = 5 | ||||||
|     let day = 1 |     let day = 1 | ||||||
|     if(!isNaN(Number(process.argv[2]))){ |     if (!isNaN(Number(process.argv[2]))) { | ||||||
|         year = 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]) |         month = Number(process.argv[3]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if(!isNaN(Number(process.argv[4]))){ |     if (!isNaN(Number(process.argv[4]))) { | ||||||
|         day = Number(process.argv[4]) |         day = Number(process.argv[4]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     do { |     do { | ||||||
|         try { |         try { | ||||||
| 
 |  | ||||||
|             await new StatsDownloader(targetDir).DownloadStats(year, month, day) |             await new StatsDownloader(targetDir).DownloadStats(year, month, day) | ||||||
|             break |             break | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.log(e) |             console.log(e) | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } while (true) |     } while (true) | ||||||
|     const allPaths = readdirSync(targetDir) |     const allPaths = readdirSync(targetDir).filter( | ||||||
|         .filter(p => p.startsWith("stats.") && p.endsWith(".json")); |         (p) => p.startsWith("stats.") && p.endsWith(".json") | ||||||
|     let allFeatures: ChangeSetData[] = [].concat(...allPaths |     ) | ||||||
|         .map(path => JSON.parse(readFileSync("Docs/Tools/stats/" + path, "utf-8")).features)); |     let allFeatures: ChangeSetData[] = [].concat( | ||||||
|     allFeatures = allFeatures.filter(f => f?.properties !== undefined && (f.properties.editor === null || f.properties.editor.toLowerCase().startsWith("mapcomplete"))) |         ...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) { |     if (process.argv.indexOf("--no-graphs") >= 0) { | ||||||
|         return |         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)) |     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 BaseLayer from "../../Models/BaseLayer" | ||||||
| import {ImmutableStore, Store, UIEventSource} from "../UIEventSource"; | import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc" | ||||||
| 
 | 
 | ||||||
| export interface AvailableBaseLayersObj { | export interface AvailableBaseLayersObj { | ||||||
|     readonly osmCarto: BaseLayer; |     readonly osmCarto: BaseLayer | ||||||
|     layerOverview: BaseLayer[]; |     layerOverview: BaseLayer[] | ||||||
| 
 | 
 | ||||||
|     AvailableLayersAt(location: Store<Loc>): Store<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 |  * Changes the basemap | ||||||
|  */ |  */ | ||||||
| export default class AvailableBaseLayers { | export default class AvailableBaseLayers { | ||||||
| 
 |     public static layerOverview: BaseLayer[] | ||||||
| 
 |     public static osmCarto: BaseLayer | ||||||
|     public static layerOverview: BaseLayer[]; |  | ||||||
|     public static osmCarto: BaseLayer; |  | ||||||
| 
 | 
 | ||||||
|     private static implementation: AvailableBaseLayersObj |     private static implementation: AvailableBaseLayersObj | ||||||
| 
 | 
 | ||||||
|     static AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> { |     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> { |     static SelectBestLayerAccordingTo( | ||||||
|         return AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(location, preferedCategory) ?? new ImmutableStore<BaseLayer>(undefined); |         location: Store<Loc>, | ||||||
| 
 |         preferedCategory: UIEventSource<string | string[]> | ||||||
|  |     ): Store<BaseLayer> { | ||||||
|  |         return ( | ||||||
|  |             AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo( | ||||||
|  |                 location, | ||||||
|  |                 preferedCategory | ||||||
|  |             ) ?? new ImmutableStore<BaseLayer>(undefined) | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static implement(backend: AvailableBaseLayersObj) { |     public static implement(backend: AvailableBaseLayersObj) { | ||||||
|  | @ -38,5 +48,4 @@ export default class AvailableBaseLayers { | ||||||
|         AvailableBaseLayers.osmCarto = backend.osmCarto |         AvailableBaseLayers.osmCarto = backend.osmCarto | ||||||
|         AvailableBaseLayers.implementation = backend |         AvailableBaseLayers.implementation = backend | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,66 +1,77 @@ | ||||||
| import BaseLayer from "../../Models/BaseLayer"; | import BaseLayer from "../../Models/BaseLayer" | ||||||
| import {Store, Stores} from "../UIEventSource"; | import { Store, Stores } from "../UIEventSource" | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc" | ||||||
| import {GeoOperations} from "../GeoOperations"; | import { GeoOperations } from "../GeoOperations" | ||||||
| import * as editorlayerindex from "../../assets/editor-layer-index.json"; | import * as editorlayerindex from "../../assets/editor-layer-index.json" | ||||||
| import * as L from "leaflet"; | import * as L from "leaflet" | ||||||
| import {TileLayer} from "leaflet"; | import { TileLayer } from "leaflet" | ||||||
| import * as X from "leaflet-providers"; | import * as X from "leaflet-providers" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import {AvailableBaseLayersObj} from "./AvailableBaseLayers"; | import { AvailableBaseLayersObj } from "./AvailableBaseLayers" | ||||||
| import {BBox} from "../BBox"; | import { BBox } from "../BBox" | ||||||
| 
 | 
 | ||||||
| export default class AvailableBaseLayersImplementation implements AvailableBaseLayersObj { | export default class AvailableBaseLayersImplementation implements AvailableBaseLayersObj { | ||||||
| 
 |     public readonly osmCarto: BaseLayer = { | ||||||
|     public readonly osmCarto: BaseLayer = |         id: "osm", | ||||||
|         { |         name: "OpenStreetMap", | ||||||
|             id: "osm", |         layer: () => | ||||||
|             name: "OpenStreetMap", |             AvailableBaseLayersImplementation.CreateBackgroundLayer( | ||||||
|             layer: () => AvailableBaseLayersImplementation.CreateBackgroundLayer("osm", "OpenStreetMap", |                 "osm", | ||||||
|                 "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright", |                 "OpenStreetMap", | ||||||
|  |                 "https://tile.openstreetmap.org/{z}/{x}/{y}.png", | ||||||
|  |                 "OpenStreetMap", | ||||||
|  |                 "https://openStreetMap.org/copyright", | ||||||
|                 19, |                 19, | ||||||
|                 false, false), |                 false, | ||||||
|             feature: null, |                 false | ||||||
|             max_zoom: 19, |             ), | ||||||
|             min_zoom: 0, |         feature: null, | ||||||
|             isBest: true, // Of course, OpenStreetMap is the best map!
 |         max_zoom: 19, | ||||||
|             category: "osmbasedmap" |         min_zoom: 0, | ||||||
|         } |         isBest: true, // Of course, OpenStreetMap is the best map!
 | ||||||
|  |         category: "osmbasedmap", | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     public readonly layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(AvailableBaseLayersImplementation.LoadProviderIndex()); |     public readonly layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat( | ||||||
|     public readonly globalLayers = this.layerOverview.filter(layer => layer.feature?.geometry === undefined || layer.feature?.geometry === null) |         AvailableBaseLayersImplementation.LoadProviderIndex() | ||||||
|     public readonly localLayers = this.layerOverview.filter(layer => layer.feature?.geometry !== undefined && layer.feature?.geometry !== null) |     ) | ||||||
|  |     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[] { |     private static LoadRasterIndex(): BaseLayer[] { | ||||||
|         const layers: BaseLayer[] = [] |         const layers: BaseLayer[] = [] | ||||||
|         // @ts-ignore
 |         // @ts-ignore
 | ||||||
|         const features = editorlayerindex.features; |         const features = editorlayerindex.features | ||||||
|         for (const i in features) { |         for (const i in features) { | ||||||
|             const layer = features[i]; |             const layer = features[i] | ||||||
|             const props = layer.properties; |             const props = layer.properties | ||||||
| 
 | 
 | ||||||
|             if (props.type === "bing") { |             if (props.type === "bing") { | ||||||
|                 // A lot of work to implement - see https://github.com/pietervdvn/MapComplete/issues/648
 |                 // A lot of work to implement - see https://github.com/pietervdvn/MapComplete/issues/648
 | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (props.id === "MAPNIK") { |             if (props.id === "MAPNIK") { | ||||||
|                 // Already added by default
 |                 // Already added by default
 | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (props.overlay) { |             if (props.overlay) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (props.url.toLowerCase().indexOf("apikey") > 0) { |             if (props.url.toLowerCase().indexOf("apikey") > 0) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (props.max_zoom < 19) { |             if (props.max_zoom < 19) { | ||||||
|                 // We want users to zoom to level 19 when adding a point
 |                 // 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
 |                 // 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) { |             if (props.name === undefined) { | ||||||
|  | @ -68,17 +79,17 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| 
 |             const leafletLayer: () => TileLayer = () => | ||||||
|             const leafletLayer: () => TileLayer = () => AvailableBaseLayersImplementation.CreateBackgroundLayer( |                 AvailableBaseLayersImplementation.CreateBackgroundLayer( | ||||||
|                 props.id, |                     props.id, | ||||||
|                 props.name, |                     props.name, | ||||||
|                 props.url, |                     props.url, | ||||||
|                 props.name, |                     props.name, | ||||||
|                 props.license_url, |                     props.license_url, | ||||||
|                 props.max_zoom, |                     props.max_zoom, | ||||||
|                 props.type.toLowerCase() === "wms", |                     props.type.toLowerCase() === "wms", | ||||||
|                 props.type.toLowerCase() === "wmts" |                     props.type.toLowerCase() === "wmts" | ||||||
|             ) |                 ) | ||||||
| 
 | 
 | ||||||
|             // Note: if layer.geometry is null, there is global coverage for this layer
 |             // Note: if layer.geometry is null, there is global coverage for this layer
 | ||||||
|             layers.push({ |             layers.push({ | ||||||
|  | @ -89,34 +100,35 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL | ||||||
|                 layer: leafletLayer, |                 layer: leafletLayer, | ||||||
|                 feature: layer.geometry !== null ? layer : null, |                 feature: layer.geometry !== null ? layer : null, | ||||||
|                 isBest: props.best ?? false, |                 isBest: props.best ?? false, | ||||||
|                 category: props.category |                 category: props.category, | ||||||
|             }); |             }) | ||||||
|         } |         } | ||||||
|         return layers; |         return layers | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static LoadProviderIndex(): BaseLayer[] { |     private static LoadProviderIndex(): BaseLayer[] { | ||||||
|         // @ts-ignore
 |         // @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 { |         function l(id: string, name: string): BaseLayer { | ||||||
|             try { |             try { | ||||||
|                 const layer: any = L.tileLayer.provider(id, undefined); |                 const layer: any = L.tileLayer.provider(id, undefined) | ||||||
|                 return { |                 return { | ||||||
|                     feature: null, |                     feature: null, | ||||||
|                     id: id, |                     id: id, | ||||||
|                     name: name, |                     name: name, | ||||||
|                     layer: () => L.tileLayer.provider(id, { |                     layer: () => | ||||||
|                         maxNativeZoom: layer.options?.maxZoom, |                         L.tileLayer.provider(id, { | ||||||
|                        maxZoom: Math.max(layer.options?.maxZoom ?? 19, 21) |                             maxNativeZoom: layer.options?.maxZoom, | ||||||
|                     }), |                             maxZoom: Math.max(layer.options?.maxZoom ?? 19, 21), | ||||||
|  |                         }), | ||||||
|                     min_zoom: 1, |                     min_zoom: 1, | ||||||
|                     max_zoom: layer.options.maxZoom, |                     max_zoom: layer.options.maxZoom, | ||||||
|                     category: "osmbasedmap", |                     category: "osmbasedmap", | ||||||
|                     isBest: false |                     isBest: false, | ||||||
|                 } |                 } | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 console.error("Could not find provided layer", name, e); |                 console.error("Could not find provided layer", name, e) | ||||||
|                 return null; |                 return null | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -129,38 +141,50 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL | ||||||
|             l("CartoDB.PositronNoLabels", "Positron  - no labels (by CartoDB)"), |             l("CartoDB.PositronNoLabels", "Positron  - no labels (by CartoDB)"), | ||||||
|             l("CartoDB.Voyager", "Voyager (by CartoDB)"), |             l("CartoDB.Voyager", "Voyager (by CartoDB)"), | ||||||
|             l("CartoDB.VoyagerNoLabels", "Voyager  - no labels (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 |      * 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, |     private static CreateBackgroundLayer( | ||||||
|                                          maxZoom: number, isWms: boolean, isWMTS?: boolean): TileLayer { |         id: string, | ||||||
| 
 |         name: string, | ||||||
|         url = url.replace("{zoom}", "{z}") |         url: string, | ||||||
|             .replace("&BBOX={bbox}", "") |         attribution: string, | ||||||
|             .replace("&bbox={bbox}", ""); |         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:[^}]*}/) |         const subdomainsMatch = url.match(/{switch:[^}]*}/) | ||||||
|         let domains: string[] = []; |         let domains: string[] = [] | ||||||
|         if (subdomainsMatch !== null) { |         if (subdomainsMatch !== null) { | ||||||
|             let domainsStr = subdomainsMatch[0].substr("{switch:".length); |             let domainsStr = subdomainsMatch[0].substr("{switch:".length) | ||||||
|             domainsStr = domainsStr.substr(0, domainsStr.length - 1); |             domainsStr = domainsStr.substr(0, domainsStr.length - 1) | ||||||
|             domains = domainsStr.split(","); |             domains = domainsStr.split(",") | ||||||
|             url = url.replace(/{switch:[^}]*}/, "{s}") |             url = url.replace(/{switch:[^}]*}/, "{s}") | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         if (isWms) { |         if (isWms) { | ||||||
|             url = url.replace("&SRS={proj}", ""); |             url = url.replace("&SRS={proj}", "") | ||||||
|             url = url.replace("&srs={proj}", ""); |             url = url.replace("&srs={proj}", "") | ||||||
|             const paramaters = ["format", "layers", "version", "service", "request", "styles", "transparent", "version"]; |             const paramaters = [ | ||||||
|             const urlObj = new URL(url); |                 "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 = { |             const options = { | ||||||
|                 maxZoom: Math.max(maxZoom ?? 19, 21), |                 maxZoom: Math.max(maxZoom ?? 19, 21), | ||||||
|                 maxNativeZoom: maxZoom ?? 19, |                 maxNativeZoom: maxZoom ?? 19, | ||||||
|  | @ -168,116 +192,117 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL | ||||||
|                 subdomains: domains, |                 subdomains: domains, | ||||||
|                 uppercase: isUpper, |                 uppercase: isUpper, | ||||||
|                 transparent: false, |                 transparent: false, | ||||||
|             }; |             } | ||||||
| 
 | 
 | ||||||
|             for (const paramater of paramaters) { |             for (const paramater of paramaters) { | ||||||
|                 let p = paramater; |                 let p = paramater | ||||||
|                 if (isUpper) { |                 if (isUpper) { | ||||||
|                     p = paramater.toUpperCase(); |                     p = paramater.toUpperCase() | ||||||
|                 } |                 } | ||||||
|                 options[paramater] = urlObj.searchParams.get(p); |                 options[paramater] = urlObj.searchParams.get(p) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (options.transparent === null) { |             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) { |         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, | ||||||
|                 attribution: attribution, |             maxZoom: Math.max(21, maxZoom ?? 19), | ||||||
|                 maxZoom: Math.max(21, maxZoom ?? 19), |             maxNativeZoom: maxZoom ?? 19, | ||||||
|                 maxNativeZoom: maxZoom ?? 19, |             minZoom: 1, | ||||||
|                 minZoom: 1, |             // @ts-ignore
 | ||||||
|                 // @ts-ignore
 |             wmts: isWMTS ?? false, | ||||||
|                 wmts: isWMTS ?? false, |             subdomains: domains, | ||||||
|                 subdomains: domains |         }) | ||||||
|             }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> { |     public AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> { | ||||||
|         return Stores.ListStabilized(location.map( |         return Stores.ListStabilized( | ||||||
|             (currentLocation) => { |             location.map((currentLocation) => { | ||||||
|                 if (currentLocation === undefined) { |                 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> { |     public SelectBestLayerAccordingTo( | ||||||
|         return this.AvailableLayersAt(location) |         location: Store<Loc>, | ||||||
|             .map(available => { |         preferedCategory: Store<string | string[]> | ||||||
|  |     ): Store<BaseLayer> { | ||||||
|  |         return this.AvailableLayersAt(location).map( | ||||||
|  |             (available) => { | ||||||
|                 // First float all 'best layers' to the top
 |                 // First float all 'best layers' to the top
 | ||||||
|                 available.sort((a, b) => { |                 available.sort((a, b) => { | ||||||
|                         if (a.isBest && b.isBest) { |                     if (a.isBest && b.isBest) { | ||||||
|                             return 0; |                         return 0 | ||||||
|                         } |  | ||||||
|                         if (!a.isBest) { |  | ||||||
|                             return 1 |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         return -1; |  | ||||||
|                     } |                     } | ||||||
|                 ) |                     if (!a.isBest) { | ||||||
|  |                         return 1 | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     return -1 | ||||||
|  |                 }) | ||||||
| 
 | 
 | ||||||
|                 if (preferedCategory.data === undefined) { |                 if (preferedCategory.data === undefined) { | ||||||
|                     return available[0] |                     return available[0] | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 let prefered: string [] |                 let prefered: string[] | ||||||
|                 if (typeof preferedCategory.data === "string") { |                 if (typeof preferedCategory.data === "string") { | ||||||
|                     prefered = [preferedCategory.data] |                     prefered = [preferedCategory.data] | ||||||
|                 } else { |                 } 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) { |                 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
 |                     //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
 | ||||||
|                     available.sort((a, b) => { |                     available.sort((a, b) => { | ||||||
|                             if (a.category === category && b.category === category) { |                         if (a.category === category && b.category === category) { | ||||||
|                                 return 0; |                             return 0 | ||||||
|                             } |  | ||||||
|                             if (a.category !== category) { |  | ||||||
|                                 return 1 |  | ||||||
|                             } |  | ||||||
| 
 |  | ||||||
|                             return -1; |  | ||||||
|                         } |                         } | ||||||
|                     ) |                         if (a.category !== category) { | ||||||
|  |                             return 1 | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         return -1 | ||||||
|  |                     }) | ||||||
|                 } |                 } | ||||||
|                 return available[0] |                 return available[0] | ||||||
|             }, [preferedCategory]) |             }, | ||||||
|  |             [preferedCategory] | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] { |     private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] { | ||||||
|         const availableLayers = [this.osmCarto] |         const availableLayers = [this.osmCarto] | ||||||
|         if (lon === undefined || lat === undefined) { |         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) { |         for (const layerOverviewItem of this.localLayers) { | ||||||
|             const layer = layerOverviewItem; |             const layer = layerOverviewItem | ||||||
|             const bbox = BBox.get(layer.feature) |             const bbox = BBox.get(layer.feature) | ||||||
|              | 
 | ||||||
|             if(!bbox.contains(lonlat)){ |             if (!bbox.contains(lonlat)) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (GeoOperations.inside(lonlat, layer.feature)) { |             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 { UIEventSource } from "../UIEventSource" | ||||||
| import BaseLayer from "../../Models/BaseLayer"; | import BaseLayer from "../../Models/BaseLayer" | ||||||
| import AvailableBaseLayers from "./AvailableBaseLayers"; | import AvailableBaseLayers from "./AvailableBaseLayers" | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Sets the current background layer to a layer that is actually available |  * Sets the current background layer to a layer that is actually available | ||||||
|  */ |  */ | ||||||
| export default class BackgroundLayerResetter { | export default class BackgroundLayerResetter { | ||||||
| 
 |     constructor( | ||||||
|     constructor(currentBackgroundLayer: UIEventSource<BaseLayer>, |         currentBackgroundLayer: UIEventSource<BaseLayer>, | ||||||
|                 location: UIEventSource<Loc>, |         location: UIEventSource<Loc>, | ||||||
|                 availableLayers: UIEventSource<BaseLayer[]>, |         availableLayers: UIEventSource<BaseLayer[]>, | ||||||
|                 defaultLayerId: string = undefined) { |         defaultLayerId: string = undefined | ||||||
| 
 |     ) { | ||||||
|         if (Utils.runningFromConsole) { |         if (Utils.runningFromConsole) { | ||||||
|             return |             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
 |         // Change the baselayer back to OSM if we go out of the current range of the layer
 | ||||||
|         availableLayers.addCallbackAndRun(availableLayers => { |         availableLayers.addCallbackAndRun((availableLayers) => { | ||||||
|             let defaultLayer = undefined; |             let defaultLayer = undefined | ||||||
|             const currentLayer = currentBackgroundLayer.data.id; |             const currentLayer = currentBackgroundLayer.data.id | ||||||
|             for (const availableLayer of availableLayers) { |             for (const availableLayer of availableLayers) { | ||||||
|                 if (availableLayer.id === currentLayer) { |                 if (availableLayer.id === currentLayer) { | ||||||
| 
 |  | ||||||
|                     if (availableLayer.max_zoom < location.data.zoom) { |                     if (availableLayer.max_zoom < location.data.zoom) { | ||||||
|                         break; |                         break | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     if (availableLayer.min_zoom > location.data.zoom) { |                     if (availableLayer.min_zoom > location.data.zoom) { | ||||||
|                         break; |                         break | ||||||
|                     } |                     } | ||||||
|                     if (availableLayer.id === defaultLayerId) { |                     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!
 |             // 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") |             console.log( | ||||||
|             currentBackgroundLayer.setData(defaultLayer ?? AvailableBaseLayers.osmCarto); |                 "AvailableBaseLayers-actor: detected that the current bounds aren't sufficient anymore - reverting to OSM standard" | ||||||
|         }); |             ) | ||||||
| 
 |             currentBackgroundLayer.setData(defaultLayer ?? AvailableBaseLayers.osmCarto) | ||||||
|  |         }) | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,36 +1,34 @@ | ||||||
| import {ElementStorage} from "../ElementStorage"; | import { ElementStorage } from "../ElementStorage" | ||||||
| import {Changes} from "../Osm/Changes"; | import { Changes } from "../Osm/Changes" | ||||||
| 
 | 
 | ||||||
| export default class ChangeToElementsActor { | export default class ChangeToElementsActor { | ||||||
|     constructor(changes: Changes, allElements: ElementStorage) { |     constructor(changes: Changes, allElements: ElementStorage) { | ||||||
|         changes.pendingChanges.addCallbackAndRun(changes => { |         changes.pendingChanges.addCallbackAndRun((changes) => { | ||||||
|             for (const change of changes) { |             for (const change of changes) { | ||||||
|                 const id = change.type + "/" + change.id; |                 const id = change.type + "/" + change.id | ||||||
|                 if (!allElements.has(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) |                 const src = allElements.getEventSourceById(id) | ||||||
| 
 | 
 | ||||||
|                 let changed = false; |                 let changed = false | ||||||
|                 for (const kv of change.tags ?? []) { |                 for (const kv of change.tags ?? []) { | ||||||
|                     // Apply tag changes and ping the consumers
 |                     // Apply tag changes and ping the consumers
 | ||||||
|                     const k = kv.k |                     const k = kv.k | ||||||
|                     let v = kv.v |                     let v = kv.v | ||||||
|                     if (v === "") { |                     if (v === "") { | ||||||
|                         v = undefined; |                         v = undefined | ||||||
|                     } |                     } | ||||||
|                     if (src.data[k] === v) { |                     if (src.data[k] === v) { | ||||||
|                         continue |                         continue | ||||||
|                     } |                     } | ||||||
|                     changed = true; |                     changed = true | ||||||
|                     src.data[k] = v; |                     src.data[k] = v | ||||||
|                 } |                 } | ||||||
|                 if (changed) { |                 if (changed) { | ||||||
|                     src.ping() |                     src.ping() | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,60 +1,59 @@ | ||||||
| import {Store, UIEventSource} from "../UIEventSource"; | import { Store, UIEventSource } from "../UIEventSource" | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg" | ||||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||||
| import {VariableUiElement} from "../../UI/Base/VariableUIElement"; | import { VariableUiElement } from "../../UI/Base/VariableUIElement" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import {QueryParameters} from "../Web/QueryParameters"; | import { QueryParameters } from "../Web/QueryParameters" | ||||||
| import {BBox} from "../BBox"; | import { BBox } from "../BBox" | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants" | ||||||
| import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"; | import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource" | ||||||
| 
 | 
 | ||||||
| export interface GeoLocationPointProperties  { | export interface GeoLocationPointProperties { | ||||||
|     id: "gps", |     id: "gps" | ||||||
|     "user:location": "yes", |     "user:location": "yes" | ||||||
|     "date": string, |     date: string | ||||||
|     "latitude": number |     latitude: number | ||||||
|     "longitude": number, |     longitude: number | ||||||
|     "speed": number, |     speed: number | ||||||
|     "accuracy": number |     accuracy: number | ||||||
|     "heading": number |     heading: number | ||||||
|     "altitude": number |     altitude: number | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default class GeoLocationHandler extends VariableUiElement { | export default class GeoLocationHandler extends VariableUiElement { | ||||||
| 
 |  | ||||||
|     private readonly currentLocation?: SimpleFeatureSource |     private readonly currentLocation?: SimpleFeatureSource | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Wether or not the geolocation is active, aka the user requested the current location |      * 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 |      * 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 |      * The callback over the permission API | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private readonly _permission: UIEventSource<string>; |     private readonly _permission: UIEventSource<string> | ||||||
|     /** |     /** | ||||||
|      * Literally: _currentGPSLocation.data != undefined |      * Literally: _currentGPSLocation.data != undefined | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private readonly _hasLocation: Store<boolean>; |     private readonly _hasLocation: Store<boolean> | ||||||
|     private readonly _currentGPSLocation: UIEventSource<GeolocationCoordinates>; |     private readonly _currentGPSLocation: UIEventSource<GeolocationCoordinates> | ||||||
|     /** |     /** | ||||||
|      * Kept in order to update the marker |      * Kept in order to update the marker | ||||||
|      * @private |      * @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 |      * 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. |      * 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 |      * If the user denies the geolocation this time, we unset this flag | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private readonly _previousLocationGrant: UIEventSource<string>; |     private readonly _previousLocationGrant: UIEventSource<string> | ||||||
|     private readonly _layoutToUse: LayoutConfig; |     private readonly _layoutToUse: LayoutConfig | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor(state: { | ||||||
|         state: { |         selectedElement: UIEventSource<any> | ||||||
|             selectedElement: UIEventSource<any>; |         currentUserLocation?: SimpleFeatureSource | ||||||
|             currentUserLocation?: SimpleFeatureSource, |         leafletMap: UIEventSource<any> | ||||||
|             leafletMap: UIEventSource<any>, |         layoutToUse: LayoutConfig | ||||||
|             layoutToUse: LayoutConfig, |         featureSwitchGeolocation: UIEventSource<boolean> | ||||||
|             featureSwitchGeolocation: UIEventSource<boolean> |     }) { | ||||||
|         } |         const currentGPSLocation = new UIEventSource<GeolocationCoordinates>( | ||||||
|     ) { |             undefined, | ||||||
|         const currentGPSLocation = new UIEventSource<GeolocationCoordinates>(undefined, "GPS-coordinate") |             "GPS-coordinate" | ||||||
|  |         ) | ||||||
|         const leafletMap = state.leafletMap |         const leafletMap = state.leafletMap | ||||||
|         const initedAt = new Date() |         const initedAt = new Date() | ||||||
|         let autozoomDone = false; |         let autozoomDone = false | ||||||
|         const hasLocation = currentGPSLocation.map( |         const hasLocation = currentGPSLocation.map((location) => location !== undefined) | ||||||
|             (location) => location !== undefined |         const previousLocationGrant = LocalStorageSource.Get("geolocation-permissions") | ||||||
|         ); |         const isActive = new UIEventSource<boolean>(false) | ||||||
|         const previousLocationGrant = LocalStorageSource.Get( |         const isLocked = new UIEventSource<boolean>(false) | ||||||
|             "geolocation-permissions" |         const permission = new UIEventSource<string>("") | ||||||
|         ); |         const lastClick = new UIEventSource<Date>(undefined) | ||||||
|         const isActive = new UIEventSource<boolean>(false); |         const lastClickWithinThreeSecs = lastClick.map((lastClick) => { | ||||||
|         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) { |             if (lastClick === undefined) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|             const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000 |             const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000 | ||||||
|             return timeDiff <= 3 |             return timeDiff <= 3 | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon") |         const latLonGiven = | ||||||
|         const willFocus = lastClick.map(lastUserRequest => { |             QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon") | ||||||
|  |         const willFocus = lastClick.map((lastUserRequest) => { | ||||||
|             const timeDiffInited = (new Date().getTime() - initedAt.getTime()) / 1000 |             const timeDiffInited = (new Date().getTime() - initedAt.getTime()) / 1000 | ||||||
|             if (!latLonGiven && !autozoomDone && timeDiffInited < Constants.zoomToLocationTimeout) { |             if (!latLonGiven && !autozoomDone && timeDiffInited < Constants.zoomToLocationTimeout) { | ||||||
|                 return true |                 return true | ||||||
|             } |             } | ||||||
|             if (lastUserRequest === undefined) { |             if (lastUserRequest === undefined) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|             const timeDiff = (new Date().getTime() - lastUserRequest.getTime()) / 1000 |             const timeDiff = (new Date().getTime() - lastUserRequest.getTime()) / 1000 | ||||||
|             return timeDiff <= Constants.zoomToLocationTimeout |             return timeDiff <= Constants.zoomToLocationTimeout | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         lastClick.addCallbackAndRunD(_ => { |         lastClick.addCallbackAndRunD((_) => { | ||||||
|             window.setTimeout(() => { |             window.setTimeout(() => { | ||||||
|                 if (lastClickWithinThreeSecs.data || willFocus.data) { |                 if (lastClickWithinThreeSecs.data || willFocus.data) { | ||||||
|                     lastClick.ping() |                     lastClick.ping() | ||||||
|  | @ -123,7 +120,7 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|             hasLocation.map( |             hasLocation.map( | ||||||
|                 (hasLocationData) => { |                 (hasLocationData) => { | ||||||
|                     if (permission.data === "denied") { |                     if (permission.data === "denied") { | ||||||
|                         return Svg.location_refused_svg(); |                         return Svg.location_refused_svg() | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     if (!isActive.data) { |                     if (!isActive.data) { | ||||||
|  | @ -134,7 +131,7 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|                         // If will focus is active too, we indicate this differently
 |                         // If will focus is active too, we indicate this differently
 | ||||||
|                         const icon = willFocus.data ? Svg.location_svg() : Svg.location_empty_svg() |                         const icon = willFocus.data ? Svg.location_svg() : Svg.location_empty_svg() | ||||||
|                         icon.SetStyle("animation: spin 4s linear infinite;") |                         icon.SetStyle("animation: spin 4s linear infinite;") | ||||||
|                         return icon; |                         return icon | ||||||
|                     } |                     } | ||||||
|                     if (isLocked.data) { |                     if (isLocked.data) { | ||||||
|                         return Svg.location_locked_svg() |                         return Svg.location_locked_svg() | ||||||
|  | @ -144,42 +141,41 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     // We have a location, so we show a dot in the center
 |                     // 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] |                 [isActive, isLocked, permission, lastClickWithinThreeSecs, willFocus] | ||||||
|             ) |             ) | ||||||
|         ); |         ) | ||||||
|         this.SetClass("mapcontrol") |         this.SetClass("mapcontrol") | ||||||
|         this._isActive = isActive; |         this._isActive = isActive | ||||||
|         this._isLocked = isLocked; |         this._isLocked = isLocked | ||||||
|         this._permission = permission |         this._permission = permission | ||||||
|         this._previousLocationGrant = previousLocationGrant; |         this._previousLocationGrant = previousLocationGrant | ||||||
|         this._currentGPSLocation = currentGPSLocation; |         this._currentGPSLocation = currentGPSLocation | ||||||
|         this._leafletMap = leafletMap; |         this._leafletMap = leafletMap | ||||||
|         this._layoutToUse = state.layoutToUse; |         this._layoutToUse = state.layoutToUse | ||||||
|         this._hasLocation = hasLocation; |         this._hasLocation = hasLocation | ||||||
|         this._lastUserRequest = lastClick |         this._lastUserRequest = lastClick | ||||||
|         const self = this; |         const self = this | ||||||
| 
 | 
 | ||||||
|         const currentPointer = this._isActive.map( |         const currentPointer = this._isActive.map( | ||||||
|             (isActive) => { |             (isActive) => { | ||||||
|                 if (isActive && !self._hasLocation.data) { |                 if (isActive && !self._hasLocation.data) { | ||||||
|                     return "cursor-wait"; |                     return "cursor-wait" | ||||||
|                 } |                 } | ||||||
|                 return "cursor-pointer"; |                 return "cursor-pointer" | ||||||
|             }, |             }, | ||||||
|             [this._hasLocation] |             [this._hasLocation] | ||||||
|         ); |         ) | ||||||
|         currentPointer.addCallbackAndRun((pointerClass) => { |         currentPointer.addCallbackAndRun((pointerClass) => { | ||||||
|             self.RemoveClass("cursor-wait") |             self.RemoveClass("cursor-wait") | ||||||
|             self.RemoveClass("cursor-pointer") |             self.RemoveClass("cursor-pointer") | ||||||
|             self.SetClass(pointerClass); |             self.SetClass(pointerClass) | ||||||
|         }); |         }) | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|         this.onClick(() => { |         this.onClick(() => { | ||||||
|             /* |             /* | ||||||
|              * If the previous click was within 3 seconds (and we have an active location), then we lock to the location  |              * If the previous click was within 3 seconds (and we have an active location), then we lock to the location | ||||||
|              */ |              */ | ||||||
|             if (self._hasLocation.data) { |             if (self._hasLocation.data) { | ||||||
|                 if (isLocked.data) { |                 if (isLocked.data) { | ||||||
|  | @ -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 |         isLocked.addCallbackAndRunD((isLocked) => { | ||||||
|         this.init(false, doAutoZoomToLocation); |  | ||||||
| 
 |  | ||||||
|         isLocked.addCallbackAndRunD(isLocked => { |  | ||||||
|             if (isLocked) { |             if (isLocked) { | ||||||
|                 leafletMap.data?.dragging?.disable() |                 leafletMap.data?.dragging?.disable() | ||||||
|             } else { |             } else { | ||||||
|  | @ -214,47 +212,45 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
| 
 | 
 | ||||||
|         this.currentLocation = state.currentUserLocation |         this.currentLocation = state.currentUserLocation | ||||||
|         this._currentGPSLocation.addCallback((location) => { |         this._currentGPSLocation.addCallback((location) => { | ||||||
|             self._previousLocationGrant.setData("granted"); |             self._previousLocationGrant.setData("granted") | ||||||
|             const feature = { |             const feature = { | ||||||
|                 "type": "Feature", |                 type: "Feature", | ||||||
|                 properties: <GeoLocationPointProperties>{ |                 properties: <GeoLocationPointProperties>{ | ||||||
|                     id: "gps", |                     id: "gps", | ||||||
|                     "user:location": "yes", |                     "user:location": "yes", | ||||||
|                     "date": new Date().toISOString(), |                     date: new Date().toISOString(), | ||||||
|                     "latitude": location.latitude, |                     latitude: location.latitude, | ||||||
|                     "longitude": location.longitude, |                     longitude: location.longitude, | ||||||
|                     "speed": location.speed, |                     speed: location.speed, | ||||||
|                     "accuracy": location.accuracy, |                     accuracy: location.accuracy, | ||||||
|                     "heading": location.heading, |                     heading: location.heading, | ||||||
|                     "altitude": location.altitude |                     altitude: location.altitude, | ||||||
|                 }, |                 }, | ||||||
|                 geometry: { |                 geometry: { | ||||||
|                     type: "Point", |                     type: "Point", | ||||||
|                     coordinates: [location.longitude, location.latitude], |                     coordinates: [location.longitude, location.latitude], | ||||||
|                 } |                 }, | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             self.currentLocation?.features?.setData([{feature, freshness: new Date()}]) |             self.currentLocation?.features?.setData([{ feature, freshness: new Date() }]) | ||||||
| 
 | 
 | ||||||
|             if (willFocus.data) { |             if (willFocus.data) { | ||||||
|                 console.log("Zooming to user location: willFocus is set") |                 console.log("Zooming to user location: willFocus is set") | ||||||
|                 lastClick.setData(undefined); |                 lastClick.setData(undefined) | ||||||
|                 autozoomDone = true; |                 autozoomDone = true | ||||||
|                 self.MoveToCurrentLocation(16); |                 self.MoveToCurrentLocation(16) | ||||||
|             } else if (self._isLocked.data) { |             } else if (self._isLocked.data) { | ||||||
|                 self.MoveToCurrentLocation(); |                 self.MoveToCurrentLocation() | ||||||
|             } |             } | ||||||
| 
 |         }) | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private init(askPermission: boolean, zoomToLocation: boolean) { |     private init(askPermission: boolean, zoomToLocation: boolean) { | ||||||
|         const self = this; |         const self = this | ||||||
| 
 | 
 | ||||||
|         if (self._isActive.data) { |         if (self._isActive.data) { | ||||||
|             self.MoveToCurrentLocation(16); |             self.MoveToCurrentLocation(16) | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (typeof navigator === "undefined") { |         if (typeof navigator === "undefined") { | ||||||
|  | @ -262,27 +258,25 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             navigator?.permissions |             navigator?.permissions?.query({ name: "geolocation" })?.then(function (status) { | ||||||
|                 ?.query({name: "geolocation"}) |                 console.log("Geolocation permission is ", status.state) | ||||||
|                 ?.then(function (status) { |                 if (status.state === "granted") { | ||||||
|                     console.log("Geolocation permission is ", status.state); |                     self.StartGeolocating(zoomToLocation) | ||||||
|                     if (status.state === "granted") { |                 } | ||||||
|                         self.StartGeolocating(zoomToLocation); |                 self._permission.setData(status.state) | ||||||
|                     } |                 status.onchange = function () { | ||||||
|                     self._permission.setData(status.state); |                     self._permission.setData(status.state) | ||||||
|                     status.onchange = function () { |                 } | ||||||
|                         self._permission.setData(status.state); |             }) | ||||||
|                     }; |  | ||||||
|                 }); |  | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.error(e); |             console.error(e) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (askPermission) { |         if (askPermission) { | ||||||
|             self.StartGeolocating(zoomToLocation); |             self.StartGeolocating(zoomToLocation) | ||||||
|         } else if (this._previousLocationGrant.data === "granted") { |         } else if (this._previousLocationGrant.data === "granted") { | ||||||
|             this._previousLocationGrant.setData(""); |             this._previousLocationGrant.setData("") | ||||||
|             self.StartGeolocating(zoomToLocation); |             self.StartGeolocating(zoomToLocation) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -311,7 +305,7 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|      * handler._currentGPSLocation.setData(<any> {latitude : 60, longitude: 60) // out of bounds
 |      * handler._currentGPSLocation.setData(<any> {latitude : 60, longitude: 60) // out of bounds
 | ||||||
|      * handler.MoveToCurrentLocation() |      * handler.MoveToCurrentLocation() | ||||||
|      * resultingLocation // => [60, 60]
 |      * resultingLocation // => [60, 60]
 | ||||||
|      *  |      * | ||||||
|      * // should refuse to move if out of bounds
 |      * // should refuse to move if out of bounds
 | ||||||
|      * let resultingLocation = undefined |      * let resultingLocation = undefined | ||||||
|      * let resultingzoom = 1 |      * let resultingzoom = 1 | ||||||
|  | @ -322,7 +316,7 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|      *             layoutToUse: new LayoutConfig(<any>{ |      *             layoutToUse: new LayoutConfig(<any>{ | ||||||
|      *                 id: 'test', |      *                 id: 'test', | ||||||
|      *                 title: {"en":"test"} |      *                 title: {"en":"test"} | ||||||
|      *                "lockLocation": [ [ 2.1, 50.4], [6.4, 51.54 ]],  |      *                "lockLocation": [ [ 2.1, 50.4], [6.4, 51.54 ]], | ||||||
|      *                description: "A testing theme", |      *                description: "A testing theme", | ||||||
|      *                layers: [] |      *                layers: [] | ||||||
|      *             }), |      *             }), | ||||||
|  | @ -337,20 +331,20 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|      * resultingLocation // => [51.3, 4.1]
 |      * resultingLocation // => [51.3, 4.1]
 | ||||||
|      */ |      */ | ||||||
|     private MoveToCurrentLocation(targetZoom?: number) { |     private MoveToCurrentLocation(targetZoom?: number) { | ||||||
|         const location = this._currentGPSLocation.data; |         const location = this._currentGPSLocation.data | ||||||
|         this._lastUserRequest.setData(undefined); |         this._lastUserRequest.setData(undefined) | ||||||
| 
 | 
 | ||||||
|         if ( |         if ( | ||||||
|             this._currentGPSLocation.data.latitude === 0 && |             this._currentGPSLocation.data.latitude === 0 && | ||||||
|             this._currentGPSLocation.data.longitude === 0 |             this._currentGPSLocation.data.longitude === 0 | ||||||
|         ) { |         ) { | ||||||
|             console.debug("Not moving to GPS-location: it is null island"); |             console.debug("Not moving to GPS-location: it is null island") | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // We check that the GPS location is not out of bounds
 |         // We check that the GPS location is not out of bounds
 | ||||||
|         const b = this._layoutToUse.lockLocation; |         const b = this._layoutToUse.lockLocation | ||||||
|         let inRange = true; |         let inRange = true | ||||||
|         if (b) { |         if (b) { | ||||||
|             if (b !== true) { |             if (b !== true) { | ||||||
|                 // B is an array with our locklocation
 |                 // B is an array with our locklocation
 | ||||||
|  | @ -358,41 +352,44 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if (!inRange) { |         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 { |         } else { | ||||||
|             const currentZoom = this._leafletMap.data.getZoom() |             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) { |     private StartGeolocating(zoomToGPS = true) { | ||||||
|         const self = this; |         const self = this | ||||||
| 
 | 
 | ||||||
|         this._lastUserRequest.setData(zoomToGPS ? new Date() : new Date(0)) |         this._lastUserRequest.setData(zoomToGPS ? new Date() : new Date(0)) | ||||||
|         if (self._permission.data === "denied") { |         if (self._permission.data === "denied") { | ||||||
|             self._previousLocationGrant.setData(""); |             self._previousLocationGrant.setData("") | ||||||
|             self._isActive.setData(false) |             self._isActive.setData(false) | ||||||
|             return ""; |             return "" | ||||||
|         } |         } | ||||||
|         if (this._currentGPSLocation.data !== undefined) { |         if (this._currentGPSLocation.data !== undefined) { | ||||||
|             this.MoveToCurrentLocation(16); |             this.MoveToCurrentLocation(16) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (self._isActive.data) { |         if (self._isActive.data) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         self._isActive.setData(true); |         self._isActive.setData(true) | ||||||
| 
 | 
 | ||||||
|         navigator.geolocation.watchPosition( |         navigator.geolocation.watchPosition( | ||||||
|             function (position) { |             function (position) { | ||||||
|                 self._currentGPSLocation.setData(position.coords); |                 self._currentGPSLocation.setData(position.coords) | ||||||
|             }, |             }, | ||||||
|             function () { |             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 { Store, UIEventSource } from "../UIEventSource" | ||||||
| import {Or} from "../Tags/Or"; | import { Or } from "../Tags/Or" | ||||||
| import {Overpass} from "../Osm/Overpass"; | import { Overpass } from "../Osm/Overpass" | ||||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | import FeatureSource from "../FeatureSource/FeatureSource" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import {TagsFilter} from "../Tags/TagsFilter"; | import { TagsFilter } from "../Tags/TagsFilter" | ||||||
| import SimpleMetaTagger from "../SimpleMetaTagger"; | import SimpleMetaTagger from "../SimpleMetaTagger" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import RelationsTracker from "../Osm/RelationsTracker"; | import RelationsTracker from "../Osm/RelationsTracker" | ||||||
| import {BBox} from "../BBox"; | import { BBox } from "../BBox" | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc" | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants" | ||||||
| import TileFreshnessCalculator from "../FeatureSource/TileFreshnessCalculator"; | import TileFreshnessCalculator from "../FeatureSource/TileFreshnessCalculator" | ||||||
| import {Tiles} from "../../Models/TileRange"; | import { Tiles } from "../../Models/TileRange" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| export default class OverpassFeatureSource implements FeatureSource { | export default class OverpassFeatureSource implements FeatureSource { | ||||||
| 
 |  | ||||||
|     public readonly name = "OverpassFeatureSource" |     public readonly name = "OverpassFeatureSource" | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * The last loaded features of the geojson |      * 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 relationsTracker: RelationsTracker | ||||||
|     public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0); |  | ||||||
| 
 | 
 | ||||||
|     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: { |     private readonly state: { | ||||||
|         readonly locationControl: Store<Loc>, |         readonly locationControl: Store<Loc> | ||||||
|         readonly layoutToUse: LayoutConfig, |         readonly layoutToUse: LayoutConfig | ||||||
|         readonly overpassUrl: Store<string[]>; |         readonly overpassUrl: Store<string[]> | ||||||
|         readonly overpassTimeout: Store<number>; |         readonly overpassTimeout: Store<number> | ||||||
|         readonly currentBounds: Store<BBox> |         readonly currentBounds: Store<BBox> | ||||||
|     } |     } | ||||||
|     private readonly _isActive: Store<boolean> |     private readonly _isActive: Store<boolean> | ||||||
|     /** |     /** | ||||||
|      * Callback to handle all the data |      * 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 |      * Keeps track of how fresh the data is | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private readonly freshnesses: Map<string, TileFreshnessCalculator>; |     private readonly freshnesses: Map<string, TileFreshnessCalculator> | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         state: { |         state: { | ||||||
|             readonly locationControl: Store<Loc>, |             readonly locationControl: Store<Loc> | ||||||
|             readonly layoutToUse: LayoutConfig, |             readonly layoutToUse: LayoutConfig | ||||||
|             readonly overpassUrl: Store<string[]>; |             readonly overpassUrl: Store<string[]> | ||||||
|             readonly overpassTimeout: Store<number>; |             readonly overpassTimeout: Store<number> | ||||||
|             readonly overpassMaxZoom: Store<number>, |             readonly overpassMaxZoom: Store<number> | ||||||
|             readonly currentBounds: Store<BBox> |             readonly currentBounds: Store<BBox> | ||||||
|         }, |         }, | ||||||
|         options: { |         options: { | ||||||
|             padToTiles: Store<number>, |             padToTiles: Store<number> | ||||||
|             isActive?: Store<boolean>, |             isActive?: Store<boolean> | ||||||
|             relationTracker: RelationsTracker, |             relationTracker: RelationsTracker | ||||||
|             onBboxLoaded?: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void, |             onBboxLoaded?: ( | ||||||
|  |                 bbox: BBox, | ||||||
|  |                 date: Date, | ||||||
|  |                 layers: LayerConfig[], | ||||||
|  |                 zoomlevel: number | ||||||
|  |             ) => void | ||||||
|             freshnesses?: Map<string, TileFreshnessCalculator> |             freshnesses?: Map<string, TileFreshnessCalculator> | ||||||
|         }) { |         } | ||||||
| 
 |     ) { | ||||||
|         this.state = state |         this.state = state | ||||||
|         this._isActive = options.isActive; |         this._isActive = options.isActive | ||||||
|         this.onBboxLoaded = options.onBboxLoaded |         this.onBboxLoaded = options.onBboxLoaded | ||||||
|         this.relationsTracker = options.relationTracker |         this.relationsTracker = options.relationTracker | ||||||
|         this.freshnesses = options.freshnesses |         this.freshnesses = options.freshnesses | ||||||
|         const self = this; |         const self = this | ||||||
|         state.currentBounds.addCallback(_ => { |         state.currentBounds.addCallback((_) => { | ||||||
|             self.update(options.padToTiles.data) |             self.update(options.padToTiles.data) | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private GetFilter(interpreterUrl: string, layersToDownload: LayerConfig[]): Overpass { |     private GetFilter(interpreterUrl: string, layersToDownload: LayerConfig[]): Overpass { | ||||||
|         let filters: TagsFilter[] = []; |         let filters: TagsFilter[] = [] | ||||||
|         let extraScripts: string[] = []; |         let extraScripts: string[] = [] | ||||||
|         for (const layer of layersToDownload) { |         for (const layer of layersToDownload) { | ||||||
|             if (layer.source.overpassScript !== undefined) { |             if (layer.source.overpassScript !== undefined) { | ||||||
|                 extraScripts.push(layer.source.overpassScript) |                 extraScripts.push(layer.source.overpassScript) | ||||||
|             } else { |             } else { | ||||||
|                 filters.push(layer.source.osmTags); |                 filters.push(layer.source.osmTags) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         filters = Utils.NoNull(filters) |         filters = Utils.NoNull(filters) | ||||||
|         extraScripts = Utils.NoNull(extraScripts) |         extraScripts = Utils.NoNull(extraScripts) | ||||||
|         if (filters.length + extraScripts.length === 0) { |         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) { |     private update(paddedZoomLevel: number) { | ||||||
|         if (!this._isActive.data) { |         if (!this._isActive.data) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         const self = this; |         const self = this | ||||||
|         this.updateAsync(paddedZoomLevel).then(bboxDate => { |         this.updateAsync(paddedZoomLevel).then((bboxDate) => { | ||||||
|             if (bboxDate === undefined || self.onBboxLoaded === undefined) { |             if (bboxDate === undefined || self.onBboxLoaded === undefined) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             const [bbox, date, layers] = bboxDate |             const [bbox, date, layers] = bboxDate | ||||||
|             self.onBboxLoaded(bbox, date, layers, paddedZoomLevel) |             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[]]> { |     private async updateAsync(padToZoomLevel: number): Promise<[BBox, Date, LayerConfig[]]> { | ||||||
|         if (this.runningQuery.data) { |         if (this.runningQuery.data) { | ||||||
|             console.log("Still running a query, not updating"); |             console.log("Still running a query, not updating") | ||||||
|             return undefined; |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (this.timeout.data > 0) { |         if (this.timeout.data > 0) { | ||||||
|             console.log("Still in timeout - not updating") |             console.log("Still in timeout - not updating") | ||||||
|             return undefined; |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let data: any = undefined |         let data: any = undefined | ||||||
|         let date: Date = undefined |         let date: Date = undefined | ||||||
|         let lastUsed = 0; |         let lastUsed = 0 | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|         const layersToDownload = [] |         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) { |         for (const layer of this.state.layoutToUse.layers) { | ||||||
| 
 |             if (typeof layer === "string") { | ||||||
|             if (typeof (layer) === "string") { |  | ||||||
|                 throw "A layer was not expanded!" |                 throw "A layer was not expanded!" | ||||||
|             } |             } | ||||||
|             if (Constants.priviliged_layers.indexOf(layer.id) >= 0) { |             if (Constants.priviliged_layers.indexOf(layer.id) >= 0) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             if (this.state.locationControl.data.zoom < layer.minzoom) { |             if (this.state.locationControl.data.zoom < layer.minzoom) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
|             if (layer.doNotDownload) { |             if (layer.doNotDownload) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
|             if (layer.source.geojsonSource !== undefined) { |             if (layer.source.geojsonSource !== undefined) { | ||||||
|                 // Not our responsibility to download this layer!
 |                 // Not our responsibility to download this layer!
 | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
|             const freshness = this.freshnesses?.get(layer.id) |             const freshness = this.freshnesses?.get(layer.id) | ||||||
|             if (freshness !== undefined) { |             if (freshness !== undefined) { | ||||||
|                 const oldestDataDate = Math.min(...Tiles.MapRange(neededTiles, (x, y) => { |                 const oldestDataDate = | ||||||
|                     const date = freshness.freshnessFor(padToZoomLevel, x, y); |                     Math.min( | ||||||
|                     if (date === undefined) { |                         ...Tiles.MapRange(neededTiles, (x, y) => { | ||||||
|                         return 0 |                             const date = freshness.freshnessFor(padToZoomLevel, x, y) | ||||||
|                     } |                             if (date === undefined) { | ||||||
|                     return date.getTime() |                                 return 0 | ||||||
|                 })) / 1000; |                             } | ||||||
|  |                             return date.getTime() | ||||||
|  |                         }) | ||||||
|  |                     ) / 1000 | ||||||
|                 const now = new Date().getTime() |                 const now = new Date().getTime() | ||||||
|                 const minRequiredAge = (now / 1000) - layer.maxAgeOfCache |                 const minRequiredAge = now / 1000 - layer.maxAgeOfCache | ||||||
|                 if (oldestDataDate >= minRequiredAge) { |                 if (oldestDataDate >= minRequiredAge) { | ||||||
|                     // still fresh enough - not updating
 |                     // still fresh enough - not updating
 | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             layersToDownload.push(layer) |             layersToDownload.push(layer) | ||||||
|  | @ -172,34 +186,35 @@ export default class OverpassFeatureSource implements FeatureSource { | ||||||
| 
 | 
 | ||||||
|         if (layersToDownload.length == 0) { |         if (layersToDownload.length == 0) { | ||||||
|             console.debug("Not updating - no layers needed") |             console.debug("Not updating - no layers needed") | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const self = this; |         const self = this | ||||||
|         const overpassUrls = self.state.overpassUrl.data |         const overpassUrls = self.state.overpassUrl.data | ||||||
|         let bounds: BBox |         let bounds: BBox | ||||||
|         do { |         do { | ||||||
|             try { |             try { | ||||||
| 
 |                 bounds = this.state.currentBounds.data | ||||||
|                 bounds = this.state.currentBounds.data?.pad(this.state.layoutToUse.widenFactor)?.expandToTileBounds(padToZoomLevel); |                     ?.pad(this.state.layoutToUse.widenFactor) | ||||||
|  |                     ?.expandToTileBounds(padToZoomLevel) | ||||||
| 
 | 
 | ||||||
|                 if (bounds === undefined) { |                 if (bounds === undefined) { | ||||||
|                     return undefined; |                     return undefined | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload); |                 const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload) | ||||||
| 
 | 
 | ||||||
|                 if (overpass === undefined) { |                 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) |                 console.log("Querying overpass is done", data) | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 self.retries.data++; |                 self.retries.data++ | ||||||
|                 self.retries.ping(); |                 self.retries.ping() | ||||||
|                 console.error(`QUERY FAILED due to`, e); |                 console.error(`QUERY FAILED due to`, e) | ||||||
| 
 | 
 | ||||||
|                 await Utils.waitFor(1000) |                 await Utils.waitFor(1000) | ||||||
| 
 | 
 | ||||||
|  | @ -208,34 +223,38 @@ export default class OverpassFeatureSource implements FeatureSource { | ||||||
|                     console.log("Trying next time with", overpassUrls[lastUsed]) |                     console.log("Trying next time with", overpassUrls[lastUsed]) | ||||||
|                 } else { |                 } else { | ||||||
|                     lastUsed = 0 |                     lastUsed = 0 | ||||||
|                     self.timeout.setData(self.retries.data * 5); |                     self.timeout.setData(self.retries.data * 5) | ||||||
| 
 | 
 | ||||||
|                     while (self.timeout.data > 0) { |                     while (self.timeout.data > 0) { | ||||||
|                         await Utils.waitFor(1000) |                         await Utils.waitFor(1000) | ||||||
|                         console.log(self.timeout.data) |                         console.log(self.timeout.data) | ||||||
|                         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 { |         try { | ||||||
|             if (data === undefined) { |             if (data === undefined) { | ||||||
|                 return undefined |                 return undefined | ||||||
|             } |             } | ||||||
|             data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date, undefined, this.state)); |             data.features.forEach((feature) => | ||||||
|             self.features.setData(data.features.map(f => ({feature: f, freshness: date}))); |                 SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature( | ||||||
|             return [bounds, date, layersToDownload]; |                     feature, | ||||||
|  |                     date, | ||||||
|  |                     undefined, | ||||||
|  |                     this.state | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             self.features.setData(data.features.map((f) => ({ feature: f, freshness: date }))) | ||||||
|  |             return [bounds, date, layersToDownload] | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.error("Got the overpass response, but could not process it: ", e, e.stack) |             console.error("Got the overpass response, but could not process it: ", e, e.stack) | ||||||
|             return undefined |             return undefined | ||||||
|         } finally { |         } finally { | ||||||
|             self.retries.setData(0); |             self.retries.setData(0) | ||||||
|             self.runningQuery.setData(false); |             self.runningQuery.setData(false) | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,46 +1,42 @@ | ||||||
| import {Changes} from "../Osm/Changes"; | import { Changes } from "../Osm/Changes" | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants" | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| export default class PendingChangesUploader { | export default class PendingChangesUploader { | ||||||
| 
 |     private lastChange: Date | ||||||
|     private lastChange: Date; |  | ||||||
| 
 | 
 | ||||||
|     constructor(changes: Changes, selectedFeature: UIEventSource<any>) { |     constructor(changes: Changes, selectedFeature: UIEventSource<any>) { | ||||||
|         const self = this; |         const self = this | ||||||
|         this.lastChange = new Date(); |         this.lastChange = new Date() | ||||||
|         changes.pendingChanges.addCallback(() => { |         changes.pendingChanges.addCallback(() => { | ||||||
|             self.lastChange = new Date(); |             self.lastChange = new Date() | ||||||
| 
 | 
 | ||||||
|             window.setTimeout(() => { |             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) { |                 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 |             if (feature === undefined) { | ||||||
|             .stabilized(10000) |                 // The popup got closed - we flush
 | ||||||
|             .addCallback(feature => { |                 changes.flushChanges("Flushing changes due to popup closed") | ||||||
|                 if (feature === undefined) { |             } | ||||||
|                     // The popup got closed - we flush
 |         }) | ||||||
|                     changes.flushChanges("Flushing changes due to popup closed"); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
| 
 | 
 | ||||||
|         if (Utils.runningFromConsole) { |         if (Utils.runningFromConsole) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         document.addEventListener('mouseout', e => { |         document.addEventListener("mouseout", (e) => { | ||||||
|             // @ts-ignore
 |             // @ts-ignore
 | ||||||
|             if (!e.toElement && !e.relatedTarget) { |             if (!e.toElement && !e.relatedTarget) { | ||||||
|                 changes.flushChanges("Flushing changes due to focus lost"); |                 changes.flushChanges("Flushing changes due to focus lost") | ||||||
|             } |             } | ||||||
|         }); |         }) | ||||||
| 
 | 
 | ||||||
|         document.onfocus = () => { |         document.onfocus = () => { | ||||||
|             changes.flushChanges("OnFocus") |             changes.flushChanges("OnFocus") | ||||||
|  | @ -50,28 +46,28 @@ export default class PendingChangesUploader { | ||||||
|             changes.flushChanges("OnFocus") |             changes.flushChanges("OnFocus") | ||||||
|         } |         } | ||||||
|         try { |         try { | ||||||
|             document.addEventListener("visibilitychange", () => { |             document.addEventListener( | ||||||
|                 changes.flushChanges("Visibility change") |                 "visibilitychange", | ||||||
|             }, false); |                 () => { | ||||||
|  |                     changes.flushChanges("Visibility change") | ||||||
|  |                 }, | ||||||
|  |                 false | ||||||
|  |             ) | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.warn("Could not register visibility change listener", e) |             console.warn("Could not register visibility change listener", e) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         function onunload(e) { |         function onunload(e) { | ||||||
|             if (changes.pendingChanges.data.length == 0) { |             if (changes.pendingChanges.data.length == 0) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             changes.flushChanges("onbeforeunload - probably closing or something similar"); |             changes.flushChanges("onbeforeunload - probably closing or something similar") | ||||||
|             e.preventDefault(); |             e.preventDefault() | ||||||
|             return "Saving your last changes..." |             return "Saving your last changes..." | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         window.onbeforeunload = onunload |         window.onbeforeunload = onunload | ||||||
|         // https://stackoverflow.com/questions/3239834/window-onbeforeunload-not-working-on-the-ipad#4824156
 |         // https://stackoverflow.com/questions/3239834/window-onbeforeunload-not-working-on-the-ipad#4824156
 | ||||||
|         window.addEventListener("pagehide", onunload) |         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. |  * This actor will download the latest version of the selected element from OSM and update the tags if necessary. | ||||||
|  */ |  */ | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import {ElementStorage} from "../ElementStorage"; | import { ElementStorage } from "../ElementStorage" | ||||||
| import {Changes} from "../Osm/Changes"; | import { Changes } from "../Osm/Changes" | ||||||
| import {OsmObject} from "../Osm/OsmObject"; | import { OsmObject } from "../Osm/OsmObject" | ||||||
| import {OsmConnection} from "../Osm/OsmConnection"; | import { OsmConnection } from "../Osm/OsmConnection" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import SimpleMetaTagger from "../SimpleMetaTagger"; | import SimpleMetaTagger from "../SimpleMetaTagger" | ||||||
| 
 | 
 | ||||||
| export default class SelectedElementTagsUpdater { | export default class SelectedElementTagsUpdater { | ||||||
| 
 |     private static readonly metatags = new Set([ | ||||||
|     private static readonly metatags = new Set(["timestamp", |         "timestamp", | ||||||
|         "version", |         "version", | ||||||
|         "changeset", |         "changeset", | ||||||
|         "user", |         "user", | ||||||
|         "uid", |         "uid", | ||||||
|         "id"]) |         "id", | ||||||
|  |     ]) | ||||||
| 
 | 
 | ||||||
|     constructor(state: { |     constructor(state: { | ||||||
|         selectedElement: UIEventSource<any>, |         selectedElement: UIEventSource<any> | ||||||
|         allElements: ElementStorage, |         allElements: ElementStorage | ||||||
|         changes: Changes, |         changes: Changes | ||||||
|         osmConnection: OsmConnection, |         osmConnection: OsmConnection | ||||||
|         layoutToUse: LayoutConfig |         layoutToUse: LayoutConfig | ||||||
|     }) { |     }) { | ||||||
| 
 |         state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { | ||||||
| 
 |  | ||||||
|         state.osmConnection.isLoggedIn.addCallbackAndRun(isLoggedIn => { |  | ||||||
|             if (isLoggedIn) { |             if (isLoggedIn) { | ||||||
|                 SelectedElementTagsUpdater.installCallback(state) |                 SelectedElementTagsUpdater.installCallback(state) | ||||||
|                 return true; |                 return true | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static installCallback(state: { |     public static installCallback(state: { | ||||||
|         selectedElement: UIEventSource<any>, |         selectedElement: UIEventSource<any> | ||||||
|         allElements: ElementStorage, |         allElements: ElementStorage | ||||||
|         changes: Changes, |         changes: Changes | ||||||
|         osmConnection: OsmConnection, |         osmConnection: OsmConnection | ||||||
|         layoutToUse: LayoutConfig |         layoutToUse: LayoutConfig | ||||||
|     }) { |     }) { | ||||||
| 
 |         state.selectedElement.addCallbackAndRunD((s) => { | ||||||
| 
 |  | ||||||
|         state.selectedElement.addCallbackAndRunD(s => { |  | ||||||
|             let id = s.properties?.id |             let id = s.properties?.id | ||||||
| 
 | 
 | ||||||
|             const backendUrl = state.osmConnection._oauth_config.url |             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"))) { |             if (!(id.startsWith("way") || id.startsWith("node") || id.startsWith("relation"))) { | ||||||
|                 // This object is _not_ from OSM, so we skip it!
 |                 // This object is _not_ from OSM, so we skip it!
 | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (id.indexOf("-") >= 0) { |             if (id.indexOf("-") >= 0) { | ||||||
|                 // This is a new object
 |                 // This is a new object
 | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             OsmObject.DownloadPropertiesOf(id).then(latestTags => { |             OsmObject.DownloadPropertiesOf(id).then((latestTags) => { | ||||||
|                 SelectedElementTagsUpdater.applyUpdate(state, latestTags, id) |                 SelectedElementTagsUpdater.applyUpdate(state, latestTags, id) | ||||||
|             }) |             }) | ||||||
| 
 |         }) | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static applyUpdate(state: { |     public static applyUpdate( | ||||||
|                                   selectedElement: UIEventSource<any>, |         state: { | ||||||
|                                   allElements: ElementStorage, |             selectedElement: UIEventSource<any> | ||||||
|                                   changes: Changes, |             allElements: ElementStorage | ||||||
|                                   osmConnection: OsmConnection, |             changes: Changes | ||||||
|                                   layoutToUse: LayoutConfig |             osmConnection: OsmConnection | ||||||
|                               }, latestTags: any, id: string |             layoutToUse: LayoutConfig | ||||||
|  |         }, | ||||||
|  |         latestTags: any, | ||||||
|  |         id: string | ||||||
|     ) { |     ) { | ||||||
|         try { |         try { | ||||||
| 
 |  | ||||||
|             const leftRightSensitive = state.layoutToUse.isLeftRightSensitive() |             const leftRightSensitive = state.layoutToUse.isLeftRightSensitive() | ||||||
| 
 | 
 | ||||||
|             if (leftRightSensitive) { |             if (leftRightSensitive) { | ||||||
|  | @ -87,11 +83,11 @@ export default class SelectedElementTagsUpdater { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const pendingChanges = state.changes.pendingChanges.data |             const pendingChanges = state.changes.pendingChanges.data | ||||||
|                 .filter(change => change.type + "/" + change.id === id) |                 .filter((change) => change.type + "/" + change.id === id) | ||||||
|                 .filter(change => change.tags !== undefined); |                 .filter((change) => change.tags !== undefined) | ||||||
| 
 | 
 | ||||||
|             for (const pendingChange of pendingChanges) { |             for (const pendingChange of pendingChanges) { | ||||||
|                 const tagChanges = pendingChange.tags; |                 const tagChanges = pendingChange.tags | ||||||
|                 for (const tagChange of tagChanges) { |                 for (const tagChange of tagChanges) { | ||||||
|                     const key = tagChange.k |                     const key = tagChange.k | ||||||
|                     const v = tagChange.v |                     const v = tagChange.v | ||||||
|  | @ -103,10 +99,9 @@ export default class SelectedElementTagsUpdater { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|             // With the changes applied, we merge them onto the upstream object
 |             // With the changes applied, we merge them onto the upstream object
 | ||||||
|             let somethingChanged = false; |             let somethingChanged = false | ||||||
|             const currentTagsSource = state.allElements.getEventSourceById(id); |             const currentTagsSource = state.allElements.getEventSourceById(id) | ||||||
|             const currentTags = currentTagsSource.data |             const currentTags = currentTagsSource.data | ||||||
|             for (const key in latestTags) { |             for (const key in latestTags) { | ||||||
|                 let osmValue = latestTags[key] |                 let osmValue = latestTags[key] | ||||||
|  | @ -117,7 +112,7 @@ export default class SelectedElementTagsUpdater { | ||||||
| 
 | 
 | ||||||
|                 const localValue = currentTags[key] |                 const localValue = currentTags[key] | ||||||
|                 if (localValue !== osmValue) { |                 if (localValue !== osmValue) { | ||||||
|                     somethingChanged = true; |                     somethingChanged = true | ||||||
|                     currentTags[key] = osmValue |                     currentTags[key] = osmValue | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  | @ -137,7 +132,6 @@ export default class SelectedElementTagsUpdater { | ||||||
|                 somethingChanged = true |                 somethingChanged = true | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|             if (somethingChanged) { |             if (somethingChanged) { | ||||||
|                 console.log("Detected upstream changes to the object when opening it, updating...") |                 console.log("Detected upstream changes to the object when opening it, updating...") | ||||||
|                 currentTagsSource.ping() |                 currentTagsSource.ping() | ||||||
|  | @ -148,6 +142,4 @@ export default class SelectedElementTagsUpdater { | ||||||
|             console.error("Updating the tags of selected element ", id, "failed due to", e) |             console.error("Updating the tags of selected element ", id, "failed due to", e) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,63 +1,67 @@ | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import {OsmObject} from "../Osm/OsmObject"; | import { OsmObject } from "../Osm/OsmObject" | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc" | ||||||
| import {ElementStorage} from "../ElementStorage"; | import { ElementStorage } from "../ElementStorage" | ||||||
| import FeaturePipeline from "../FeatureSource/FeaturePipeline"; | import FeaturePipeline from "../FeatureSource/FeaturePipeline" | ||||||
| import {GeoOperations} from "../GeoOperations"; | import { GeoOperations } from "../GeoOperations" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Makes sure the hash shows the selected element and vice-versa. |  * Makes sure the hash shows the selected element and vice-versa. | ||||||
|  */ |  */ | ||||||
| export default class SelectedFeatureHandler { | export default class SelectedFeatureHandler { | ||||||
|     private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "filters", "location_track", "", undefined]) |     private static readonly _no_trigger_on = new Set([ | ||||||
|     private readonly hash: UIEventSource<string>; |         "welcome", | ||||||
|  |         "copyright", | ||||||
|  |         "layers", | ||||||
|  |         "new", | ||||||
|  |         "filters", | ||||||
|  |         "location_track", | ||||||
|  |         "", | ||||||
|  |         undefined, | ||||||
|  |     ]) | ||||||
|  |     private readonly hash: UIEventSource<string> | ||||||
|     private readonly state: { |     private readonly state: { | ||||||
|         selectedElement: UIEventSource<any>, |         selectedElement: UIEventSource<any> | ||||||
|         allElements: ElementStorage, |         allElements: ElementStorage | ||||||
|         locationControl: UIEventSource<Loc>, |         locationControl: UIEventSource<Loc> | ||||||
|         layoutToUse: LayoutConfig |         layoutToUse: LayoutConfig | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         hash: UIEventSource<string>, |         hash: UIEventSource<string>, | ||||||
|         state: { |         state: { | ||||||
|             selectedElement: UIEventSource<any>, |             selectedElement: UIEventSource<any> | ||||||
|             allElements: ElementStorage, |             allElements: ElementStorage | ||||||
|             featurePipeline: FeaturePipeline, |             featurePipeline: FeaturePipeline | ||||||
|             locationControl: UIEventSource<Loc>, |             locationControl: UIEventSource<Loc> | ||||||
|             layoutToUse: LayoutConfig |             layoutToUse: LayoutConfig | ||||||
|         } |         } | ||||||
|     ) { |     ) { | ||||||
|         this.hash = hash; |         this.hash = hash | ||||||
|         this.state = state |         this.state = state | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         // If the hash changes, set the selected element correctly
 |         // If the hash changes, set the selected element correctly
 | ||||||
| 
 | 
 | ||||||
|         const self = this; |         const self = this | ||||||
|         hash.addCallback(() => self.setSelectedElementFromHash()) |         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
 |             // 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)) { |             if (hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)) { | ||||||
|                 // This is an invalid hash anyway
 |                 // This is an invalid hash anyway
 | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             if (state.selectedElement.data !== undefined) { |             if (state.selectedElement.data !== undefined) { | ||||||
|                 // We already have something selected
 |                 // We already have something selected
 | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             self.setSelectedElementFromHash() |             self.setSelectedElementFromHash() | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         this.initialLoad() |         this.initialLoad() | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * On startup: check if the hash is loaded and eventually zoom to it |      * On startup: check if the hash is loaded and eventually zoom to it | ||||||
|      * @private |      * @private | ||||||
|  | @ -65,21 +69,18 @@ export default class SelectedFeatureHandler { | ||||||
|     private initialLoad() { |     private initialLoad() { | ||||||
|         const hash = this.hash.data |         const hash = this.hash.data | ||||||
|         if (hash === undefined || hash === "" || hash.indexOf("-") >= 0) { |         if (hash === undefined || hash === "" || hash.indexOf("-") >= 0) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         if (SelectedFeatureHandler._no_trigger_on.has(hash)) { |         if (SelectedFeatureHandler._no_trigger_on.has(hash)) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!(hash.startsWith("node") || hash.startsWith("way") || hash.startsWith("relation"))) { |         if (!(hash.startsWith("node") || hash.startsWith("way") || hash.startsWith("relation"))) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |         OsmObject.DownloadObjectAsync(hash).then((obj) => { | ||||||
|         OsmObject.DownloadObjectAsync(hash).then(obj => { |  | ||||||
| 
 |  | ||||||
|             try { |             try { | ||||||
| 
 |  | ||||||
|                 console.log("Downloaded selected object from OSM-API for initial load: ", hash) |                 console.log("Downloaded selected object from OSM-API for initial load: ", hash) | ||||||
|                 const geojson = obj.asGeoJson() |                 const geojson = obj.asGeoJson() | ||||||
|                 this.state.allElements.addOrGetElement(geojson) |                 this.state.allElements.addOrGetElement(geojson) | ||||||
|  | @ -88,9 +89,7 @@ export default class SelectedFeatureHandler { | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 console.error(e) |                 console.error(e) | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private setSelectedElementFromHash() { |     private setSelectedElementFromHash() { | ||||||
|  | @ -98,22 +97,21 @@ export default class SelectedFeatureHandler { | ||||||
|         const h = this.hash.data |         const h = this.hash.data | ||||||
|         if (h === undefined || h === "") { |         if (h === undefined || h === "") { | ||||||
|             // Hash has been cleared - we clear the selected element
 |             // Hash has been cleared - we clear the selected element
 | ||||||
|             state.selectedElement.setData(undefined); |             state.selectedElement.setData(undefined) | ||||||
|         } else { |         } else { | ||||||
| 
 |  | ||||||
|             // we search the element to select
 |             // we search the element to select
 | ||||||
|             const feature = state.allElements.ContainingFeatures.get(h) |             const feature = state.allElements.ContainingFeatures.get(h) | ||||||
|             if (feature === undefined) { |             if (feature === undefined) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             const currentlySeleced = state.selectedElement.data |             const currentlySeleced = state.selectedElement.data | ||||||
|             if (currentlySeleced === undefined) { |             if (currentlySeleced === undefined) { | ||||||
|                 state.selectedElement.setData(feature) |                 state.selectedElement.setData(feature) | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             if (currentlySeleced.properties?.id === feature.properties.id) { |             if (currentlySeleced.properties?.id === feature.properties.id) { | ||||||
|                 // We already have the right feature
 |                 // We already have the right feature
 | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             state.selectedElement.setData(feature) |             state.selectedElement.setData(feature) | ||||||
|         } |         } | ||||||
|  | @ -121,25 +119,24 @@ export default class SelectedFeatureHandler { | ||||||
| 
 | 
 | ||||||
|     // If a feature is selected via the hash, zoom there
 |     // If a feature is selected via the hash, zoom there
 | ||||||
|     private zoomToSelectedFeature() { |     private zoomToSelectedFeature() { | ||||||
| 
 |  | ||||||
|         const selected = this.state.selectedElement.data |         const selected = this.state.selectedElement.data | ||||||
|         if (selected === undefined) { |         if (selected === undefined) { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const centerpoint = GeoOperations.centerpointCoordinates(selected) |         const centerpoint = GeoOperations.centerpointCoordinates(selected) | ||||||
|         const location = this.state.locationControl; |         const location = this.state.locationControl | ||||||
|         location.data.lon = centerpoint[0] |         location.data.lon = centerpoint[0] | ||||||
|         location.data.lat = centerpoint[1] |         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) { |         if (location.data.zoom < minZoom) { | ||||||
|             location.data.zoom = minZoom |             location.data.zoom = minZoom | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         location.ping(); |         location.ping() | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,88 +1,87 @@ | ||||||
| import * as L from "leaflet"; | import * as L from "leaflet" | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"; | import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen" | ||||||
| import FilteredLayer from "../../Models/FilteredLayer"; | import FilteredLayer from "../../Models/FilteredLayer" | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants" | ||||||
| import BaseUIElement from "../../UI/BaseUIElement"; | import BaseUIElement from "../../UI/BaseUIElement" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The stray-click-hanlders adds a marker to the map if no feature was clicked. |  * The stray-click-hanlders adds a marker to the map if no feature was clicked. | ||||||
|  * Shows the given uiToShow-element in the messagebox |  * Shows the given uiToShow-element in the messagebox | ||||||
|  */ |  */ | ||||||
| export default class StrayClickHandler { | export default class StrayClickHandler { | ||||||
|     private _lastMarker; |     private _lastMarker | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         state: { |         state: { | ||||||
|             LastClickLocation: UIEventSource<{ lat: number, lon: number }>, |             LastClickLocation: UIEventSource<{ lat: number; lon: number }> | ||||||
|             selectedElement: UIEventSource<string>, |             selectedElement: UIEventSource<string> | ||||||
|             filteredLayers: UIEventSource<FilteredLayer[]>, |             filteredLayers: UIEventSource<FilteredLayer[]> | ||||||
|             leafletMap: UIEventSource<L.Map> |             leafletMap: UIEventSource<L.Map> | ||||||
|         }, |         }, | ||||||
|         uiToShow: ScrollableFullScreen, |         uiToShow: ScrollableFullScreen, | ||||||
|         iconToShow: BaseUIElement) { |         iconToShow: BaseUIElement | ||||||
|         const self = this; |     ) { | ||||||
|  |         const self = this | ||||||
|         const leafletMap = state.leafletMap |         const leafletMap = state.leafletMap | ||||||
|         state.filteredLayers.data.forEach((filteredLayer) => { |         state.filteredLayers.data.forEach((filteredLayer) => { | ||||||
|             filteredLayer.isDisplayed.addCallback(isEnabled => { |             filteredLayer.isDisplayed.addCallback((isEnabled) => { | ||||||
|                 if (isEnabled && self._lastMarker && leafletMap.data !== undefined) { |                 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
 |                     // 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...
 |                     // 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) { |         state.LastClickLocation.addCallback(function (lastClick) { | ||||||
| 
 |  | ||||||
|             if (self._lastMarker !== undefined) { |             if (self._lastMarker !== undefined) { | ||||||
|                 state.leafletMap.data?.removeLayer(self._lastMarker); |                 state.leafletMap.data?.removeLayer(self._lastMarker) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (lastClick === undefined) { |             if (lastClick === undefined) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             state.selectedElement.setData(undefined); |             state.selectedElement.setData(undefined) | ||||||
|             const clickCoor: [number, number] = [lastClick.lat, lastClick.lon] |             const clickCoor: [number, number] = [lastClick.lat, lastClick.lon] | ||||||
|             self._lastMarker = L.marker(clickCoor, { |             self._lastMarker = L.marker(clickCoor, { | ||||||
|                 icon: L.divIcon({ |                 icon: L.divIcon({ | ||||||
|                     html: iconToShow.ConstructElement(), |                     html: iconToShow.ConstructElement(), | ||||||
|                     iconSize: [50, 50], |                     iconSize: [50, 50], | ||||||
|                     iconAnchor: [25, 50], |                     iconAnchor: [25, 50], | ||||||
|                     popupAnchor: [0, -45] |                     popupAnchor: [0, -45], | ||||||
|                 }) |                 }), | ||||||
|             }); |             }) | ||||||
|             const popup = L.popup({ |             const popup = L.popup({ | ||||||
|                 autoPan: true, |                 autoPan: true, | ||||||
|                 autoPanPaddingTopLeft: [15, 15], |                 autoPanPaddingTopLeft: [15, 15], | ||||||
|                 closeOnEscapeKey: true, |                 closeOnEscapeKey: true, | ||||||
|                 autoClose: true |                 autoClose: true, | ||||||
|             }).setContent("<div id='strayclick' style='height: 65vh'></div>"); |             }).setContent("<div id='strayclick' style='height: 65vh'></div>") | ||||||
|             self._lastMarker.addTo(leafletMap.data); |             self._lastMarker.addTo(leafletMap.data) | ||||||
|             self._lastMarker.bindPopup(popup); |             self._lastMarker.bindPopup(popup) | ||||||
| 
 | 
 | ||||||
|             self._lastMarker.on("click", () => { |             self._lastMarker.on("click", () => { | ||||||
|                 if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) { |                 if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) { | ||||||
|                     self._lastMarker.closePopup() |                     self._lastMarker.closePopup() | ||||||
|                     leafletMap.data.flyTo(clickCoor, Constants.userJourney.minZoomLevelToAddNewPoints) |                     leafletMap.data.flyTo( | ||||||
|                     return; |                         clickCoor, | ||||||
|  |                         Constants.userJourney.minZoomLevelToAddNewPoints | ||||||
|  |                     ) | ||||||
|  |                     return | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|                 uiToShow.AttachTo("strayclick") |                 uiToShow.AttachTo("strayclick") | ||||||
|                 uiToShow.Activate(); |                 uiToShow.Activate() | ||||||
|             }); |             }) | ||||||
|         }); |         }) | ||||||
| 
 | 
 | ||||||
|         state.selectedElement.addCallback(() => { |         state.selectedElement.addCallback(() => { | ||||||
|             if (self._lastMarker !== undefined) { |             if (self._lastMarker !== undefined) { | ||||||
|                 leafletMap.data.removeLayer(self._lastMarker); |                 leafletMap.data.removeLayer(self._lastMarker) | ||||||
|                 this._lastMarker = undefined; |                 this._lastMarker = undefined | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,19 +1,19 @@ | ||||||
| import {Store, UIEventSource} from "../UIEventSource"; | import { Store, UIEventSource } from "../UIEventSource" | ||||||
| import Locale from "../../UI/i18n/Locale"; | import Locale from "../../UI/i18n/Locale" | ||||||
| import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"; | import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer" | ||||||
| import Combine from "../../UI/Base/Combine"; | import Combine from "../../UI/Base/Combine" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import {ElementStorage} from "../ElementStorage"; | import { ElementStorage } from "../ElementStorage" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| export default class TitleHandler { | export default class TitleHandler { | ||||||
|     constructor(state: { |     constructor(state: { | ||||||
|         selectedElement: Store<any>, |         selectedElement: Store<any> | ||||||
|         layoutToUse: LayoutConfig, |         layoutToUse: LayoutConfig | ||||||
|         allElements: ElementStorage |         allElements: ElementStorage | ||||||
|     }) { |     }) { | ||||||
|         const currentTitle: Store<string> = state.selectedElement.map( |         const currentTitle: Store<string> = state.selectedElement.map( | ||||||
|             selected => { |             (selected) => { | ||||||
|                 const layout = state.layoutToUse |                 const layout = state.layoutToUse | ||||||
|                 const defaultTitle = layout?.title?.txt ?? "MapComplete" |                 const defaultTitle = layout?.title?.txt ?? "MapComplete" | ||||||
| 
 | 
 | ||||||
|  | @ -21,27 +21,32 @@ export default class TitleHandler { | ||||||
|                     return defaultTitle |                     return defaultTitle | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 const tags = selected.properties; |                 const tags = selected.properties | ||||||
|                 for (const layer of layout.layers) { |                 for (const layer of layout.layers) { | ||||||
|                     if (layer.title === undefined) { |                     if (layer.title === undefined) { | ||||||
|                         continue; |                         continue | ||||||
|                     } |                     } | ||||||
|                     if (layer.source.osmTags.matchesProperties(tags)) { |                     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, {}) |                         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 |                 return defaultTitle | ||||||
|             }, [Locale.language] |             }, | ||||||
|  |             [Locale.language] | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 |         currentTitle.addCallbackAndRunD((title) => { | ||||||
|         currentTitle.addCallbackAndRunD(title => { |  | ||||||
|             if (Utils.runningFromConsole) { |             if (Utils.runningFromConsole) { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             document.title = title |             document.title = title | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										172
									
								
								Logic/BBox.ts
									
										
									
									
									
								
							
							
						
						
									
										172
									
								
								Logic/BBox.ts
									
										
									
									
									
								
							|  | @ -1,31 +1,32 @@ | ||||||
| import * as turf from "@turf/turf"; | import * as turf from "@turf/turf" | ||||||
| import {TileRange, Tiles} from "../Models/TileRange"; | import { TileRange, Tiles } from "../Models/TileRange" | ||||||
| import {GeoOperations} from "./GeoOperations"; | import { GeoOperations } from "./GeoOperations" | ||||||
| 
 | 
 | ||||||
| export class BBox { | export class BBox { | ||||||
| 
 |     static global: BBox = new BBox([ | ||||||
|     static global: BBox = new BBox([[-180, -90], [180, 90]]); |         [-180, -90], | ||||||
|     readonly maxLat: number; |         [180, 90], | ||||||
|     readonly maxLon: number; |     ]) | ||||||
|     readonly minLat: number; |     readonly maxLat: number | ||||||
|     readonly minLon: number; |     readonly maxLon: number | ||||||
|  |     readonly minLat: number | ||||||
|  |     readonly minLon: number | ||||||
| 
 | 
 | ||||||
|     /*** |     /*** | ||||||
|      * Coordinates should be [[lon, lat],[lon, lat]] |      * Coordinates should be [[lon, lat],[lon, lat]] | ||||||
|      * @param coordinates |      * @param coordinates | ||||||
|      */ |      */ | ||||||
|     constructor(coordinates) { |     constructor(coordinates) { | ||||||
|         this.maxLat = -90; |         this.maxLat = -90 | ||||||
|         this.maxLon = -180; |         this.maxLon = -180 | ||||||
|         this.minLat = 90; |         this.minLat = 90 | ||||||
|         this.minLon = 180; |         this.minLon = 180 | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|         for (const coordinate of coordinates) { |         for (const coordinate of coordinates) { | ||||||
|             this.maxLon = Math.max(this.maxLon, coordinate[0]); |             this.maxLon = Math.max(this.maxLon, coordinate[0]) | ||||||
|             this.maxLat = Math.max(this.maxLat, coordinate[1]); |             this.maxLat = Math.max(this.maxLat, coordinate[1]) | ||||||
|             this.minLon = Math.min(this.minLon, coordinate[0]); |             this.minLon = Math.min(this.minLon, coordinate[0]) | ||||||
|             this.minLat = Math.min(this.minLat, coordinate[1]); |             this.minLat = Math.min(this.minLat, coordinate[1]) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.maxLon = Math.min(this.maxLon, 180) |         this.maxLon = Math.min(this.maxLon, 180) | ||||||
|  | @ -33,27 +34,32 @@ export class BBox { | ||||||
|         this.minLon = Math.max(this.minLon, -180) |         this.minLon = Math.max(this.minLon, -180) | ||||||
|         this.minLat = Math.max(this.minLat, -90) |         this.minLat = Math.max(this.minLat, -90) | ||||||
| 
 | 
 | ||||||
| 
 |         this.check() | ||||||
|         this.check(); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static fromLeafletBounds(bounds) { |     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 { |     static get(feature): BBox { | ||||||
|         if (feature.bbox?.overlapsWith === undefined) { |         if (feature.bbox?.overlapsWith === undefined) { | ||||||
|             const turfBbox: number[] = turf.bbox(feature) |             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 { |     static bboxAroundAll(bboxes: BBox[]): BBox { | ||||||
|         let maxLat: number = -90; |         let maxLat: number = -90 | ||||||
|         let maxLon: number = -180; |         let maxLon: number = -180 | ||||||
|         let minLat: number = 80; |         let minLat: number = 80 | ||||||
|         let minLon: number = 180; |         let minLon: number = 180 | ||||||
| 
 | 
 | ||||||
|         for (const bbox of bboxes) { |         for (const bbox of bboxes) { | ||||||
|             maxLat = Math.max(maxLat, bbox.maxLat) |             maxLat = Math.max(maxLat, bbox.maxLat) | ||||||
|  | @ -61,17 +67,20 @@ export class BBox { | ||||||
|             minLat = Math.min(minLat, bbox.minLat) |             minLat = Math.min(minLat, bbox.minLat) | ||||||
|             minLon = Math.min(minLon, bbox.minLon) |             minLon = Math.min(minLon, bbox.minLon) | ||||||
|         } |         } | ||||||
|         return new BBox([[maxLon, maxLat], [minLon, minLat]]) |         return new BBox([ | ||||||
|  |             [maxLon, maxLat], | ||||||
|  |             [minLon, minLat], | ||||||
|  |         ]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Calculates the BBox based on a slippy map tile number |      * Calculates the BBox based on a slippy map tile number | ||||||
|      *  |      * | ||||||
|      *  const bbox = BBox.fromTile(16, 32754, 21785) |      *  const bbox = BBox.fromTile(16, 32754, 21785) | ||||||
|      *  bbox.minLon // => -0.076904296875 
 |      *  bbox.minLon // => -0.076904296875
 | ||||||
|      *  bbox.maxLon // => -0.0714111328125 
 |      *  bbox.maxLon // => -0.0714111328125
 | ||||||
|      *  bbox.minLat // => 51.5292513551899 
 |      *  bbox.minLat // => 51.5292513551899
 | ||||||
|      *  bbox.maxLat // => 51.53266860674158 
 |      *  bbox.maxLat // => 51.53266860674158
 | ||||||
|      */ |      */ | ||||||
|     static fromTile(z: number, x: number, y: number): BBox { |     static fromTile(z: number, x: number, y: number): BBox { | ||||||
|         return new BBox(Tiles.tile_bounds_lon_lat(z, x, y)) |         return new BBox(Tiles.tile_bounds_lon_lat(z, x, y)) | ||||||
|  | @ -85,11 +94,10 @@ export class BBox { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public unionWith(other: BBox) { |     public unionWith(other: BBox) { | ||||||
|         return new BBox([[ |         return new BBox([ | ||||||
|             Math.max(this.maxLon, other.maxLon), |             [Math.max(this.maxLon, other.maxLon), Math.max(this.maxLat, other.maxLat)], | ||||||
|             Math.max(this.maxLat, other.maxLat)], |             [Math.min(this.minLon, other.minLon), Math.min(this.minLat, other.minLat)], | ||||||
|             [Math.min(this.minLon, other.minLon), |         ]) | ||||||
|                 Math.min(this.minLat, other.minLat)]]) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -102,32 +110,31 @@ export class BBox { | ||||||
| 
 | 
 | ||||||
|     public overlapsWith(other: BBox) { |     public overlapsWith(other: BBox) { | ||||||
|         if (this.maxLon < other.minLon) { |         if (this.maxLon < other.minLon) { | ||||||
|             return false; |             return false | ||||||
|         } |         } | ||||||
|         if (this.maxLat < other.minLat) { |         if (this.maxLat < other.minLat) { | ||||||
|             return false; |             return false | ||||||
|         } |         } | ||||||
|         if (this.minLon > other.maxLon) { |         if (this.minLon > other.maxLon) { | ||||||
|             return false; |             return false | ||||||
|         } |         } | ||||||
|         return this.minLat <= other.maxLat; |         return this.minLat <= other.maxLat | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public isContainedIn(other: BBox) { |     public isContainedIn(other: BBox) { | ||||||
|         if (this.maxLon > other.maxLon) { |         if (this.maxLon > other.maxLon) { | ||||||
|             return false; |             return false | ||||||
|         } |         } | ||||||
|         if (this.maxLat > other.maxLat) { |         if (this.maxLat > other.maxLat) { | ||||||
|             return false; |             return false | ||||||
|         } |         } | ||||||
|         if (this.minLon < other.minLon) { |         if (this.minLon < other.minLon) { | ||||||
|             return false; |             return false | ||||||
|         } |         } | ||||||
|         if (this.minLat < other.minLat) { |         if (this.minLat < other.minLat) { | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
|         return true; |         return true | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     getEast() { |     getEast() { | ||||||
|  | @ -147,32 +154,35 @@ export class BBox { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     contains(lonLat: [number, number]) { |     contains(lonLat: [number, number]) { | ||||||
|         return this.minLat <= lonLat[1] && lonLat[1] <= this.maxLat |         return ( | ||||||
|             && this.minLon <= lonLat[0] && lonLat[0] <= this.maxLon |             this.minLat <= lonLat[1] && | ||||||
|  |             lonLat[1] <= this.maxLat && | ||||||
|  |             this.minLon <= lonLat[0] && | ||||||
|  |             lonLat[0] <= this.maxLon | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pad(factor: number, maxIncrease = 2): BBox { |     pad(factor: number, maxIncrease = 2): BBox { | ||||||
| 
 |  | ||||||
|         const latDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLat - this.minLat) * factor) |         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) |         const lonDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor) | ||||||
|         return new BBox([[ |         return new BBox([ | ||||||
|             this.minLon - lonDiff, |             [this.minLon - lonDiff, this.minLat - latDiff], | ||||||
|             this.minLat - latDiff |             [this.maxLon + lonDiff, this.maxLat + latDiff], | ||||||
|         ], [this.maxLon + lonDiff, |         ]) | ||||||
|             this.maxLat + latDiff]]) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     padAbsolute(degrees: number): BBox { |     padAbsolute(degrees: number): BBox { | ||||||
| 
 |         return new BBox([ | ||||||
|         return new BBox([[ |             [this.minLon - degrees, this.minLat - degrees], | ||||||
|             this.minLon - degrees, |             [this.maxLon + degrees, this.maxLat + degrees], | ||||||
|             this.minLat - degrees |         ]) | ||||||
|         ], [this.maxLon + degrees, |  | ||||||
|             this.maxLat + degrees]]) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     toLeaflet() { |     toLeaflet() { | ||||||
|         return [[this.minLat, this.minLon], [this.maxLat, this.maxLon]] |         return [ | ||||||
|  |             [this.minLat, this.minLon], | ||||||
|  |             [this.maxLat, this.maxLon], | ||||||
|  |         ] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asGeoJson(properties: any): any { |     asGeoJson(properties: any): any { | ||||||
|  | @ -181,16 +191,16 @@ export class BBox { | ||||||
|             properties: properties, |             properties: properties, | ||||||
|             geometry: { |             geometry: { | ||||||
|                 type: "Polygon", |                 type: "Polygon", | ||||||
|                 coordinates: [[ |                 coordinates: [ | ||||||
| 
 |                     [ | ||||||
|                     [this.minLon, this.minLat], |                         [this.minLon, this.minLat], | ||||||
|                     [this.maxLon, this.minLat], |                         [this.maxLon, this.minLat], | ||||||
|                     [this.maxLon, this.maxLat], |                         [this.maxLon, this.maxLat], | ||||||
|                     [this.minLon, this.maxLat], |                         [this.minLon, this.maxLat], | ||||||
|                     [this.minLon, this.minLat], |                         [this.minLon, this.minLat], | ||||||
| 
 |                     ], | ||||||
|                 ]] |                 ], | ||||||
|             } |             }, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -206,22 +216,22 @@ export class BBox { | ||||||
|         return new BBox([].concat(boundsul, boundslr)) |         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 [minLon, minLat] = GeoOperations.ConvertWgs84To900913([this.minLon, this.minLat]) | ||||||
|         const [maxLon, maxLat] = GeoOperations.ConvertWgs84To900913([this.maxLon, this.maxLat]) |         const [maxLon, maxLat] = GeoOperations.ConvertWgs84To900913([this.maxLon, this.maxLat]) | ||||||
| 
 | 
 | ||||||
|         return { |         return { | ||||||
|             minLon, maxLon, |             minLon, | ||||||
|             minLat, maxLat |             maxLon, | ||||||
|  |             minLat, | ||||||
|  |             maxLat, | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private check() {    |     private check() { | ||||||
|         if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) { |         if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) { | ||||||
|             console.trace("BBox with NaN detected:", this); |             console.trace("BBox with NaN detected:", this) | ||||||
|             throw  "BBOX has NAN"; |             throw "BBOX has NAN" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,46 +1,56 @@ | ||||||
| /// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
 | /// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
 | ||||||
| import {Store, UIEventSource} from "./UIEventSource"; | import { Store, UIEventSource } from "./UIEventSource" | ||||||
| import FeaturePipeline from "./FeatureSource/FeaturePipeline"; | import FeaturePipeline from "./FeatureSource/FeaturePipeline" | ||||||
| import Loc from "../Models/Loc"; | import Loc from "../Models/Loc" | ||||||
| import {BBox} from "./BBox"; | import { BBox } from "./BBox" | ||||||
| 
 | 
 | ||||||
| export default class ContributorCount { | 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>()); |     constructor(state: { | ||||||
|     private readonly state: { featurePipeline: FeaturePipeline, currentBounds: Store<BBox>, locationControl: Store<Loc> }; |         featurePipeline: FeaturePipeline | ||||||
|     private lastUpdate: Date = undefined; |         currentBounds: Store<BBox> | ||||||
| 
 |         locationControl: Store<Loc> | ||||||
|     constructor(state: { featurePipeline: FeaturePipeline, currentBounds: Store<BBox>, locationControl: Store<Loc> }) { |     }) { | ||||||
|         this.state = state; |         this.state = state | ||||||
|         const self = this; |         const self = this | ||||||
|         state.currentBounds.map(bbox => { |         state.currentBounds.map((bbox) => { | ||||||
|             self.update(bbox) |             self.update(bbox) | ||||||
|         }) |         }) | ||||||
|         state.featurePipeline.runningQuery.addCallbackAndRun( |         state.featurePipeline.runningQuery.addCallbackAndRun((_) => | ||||||
|             _ => self.update(state.currentBounds.data) |             self.update(state.currentBounds.data) | ||||||
|         ) |         ) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private update(bbox: BBox) { |     private update(bbox: BBox) { | ||||||
|         if (bbox === undefined) { |         if (bbox === undefined) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         const now = new Date(); |         const now = new Date() | ||||||
|         if (this.lastUpdate !== undefined && ((now.getTime() - this.lastUpdate.getTime()) < 1000 * 60)) { |         if ( | ||||||
|             return; |             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 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 list of featuresList) { | ||||||
|             for (const feature of list) { |             for (const feature of list) { | ||||||
|                 const contributor = feature.properties["_last_edit:contributor"] |                 const contributor = feature.properties["_last_edit:contributor"] | ||||||
|                 const count = hist.get(contributor) ?? 0; |                 const count = hist.get(contributor) ?? 0 | ||||||
|                 hist.set(contributor, count + 1) |                 hist.set(contributor, count + 1) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         this.Contributors.setData(hist) |         this.Contributors.setData(hist) | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,35 +1,37 @@ | ||||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | ||||||
| import {QueryParameters} from "./Web/QueryParameters"; | import { QueryParameters } from "./Web/QueryParameters" | ||||||
| import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; | import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" | ||||||
| import {FixedUiElement} from "../UI/Base/FixedUiElement"; | import { FixedUiElement } from "../UI/Base/FixedUiElement" | ||||||
| import {Utils} from "../Utils"; | import { Utils } from "../Utils" | ||||||
| import Combine from "../UI/Base/Combine"; | import Combine from "../UI/Base/Combine" | ||||||
| import {SubtleButton} from "../UI/Base/SubtleButton"; | import { SubtleButton } from "../UI/Base/SubtleButton" | ||||||
| import BaseUIElement from "../UI/BaseUIElement"; | import BaseUIElement from "../UI/BaseUIElement" | ||||||
| import {UIEventSource} from "./UIEventSource"; | import { UIEventSource } from "./UIEventSource" | ||||||
| import {LocalStorageSource} from "./Web/LocalStorageSource"; | import { LocalStorageSource } from "./Web/LocalStorageSource" | ||||||
| import LZString from "lz-string"; | import LZString from "lz-string" | ||||||
| import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; | import { FixLegacyTheme } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert" | ||||||
| import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" | ||||||
| import SharedTagRenderings from "../Customizations/SharedTagRenderings"; | import SharedTagRenderings from "../Customizations/SharedTagRenderings" | ||||||
| import * as known_layers from "../assets/generated/known_layers.json" | 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 * as licenses from "../assets/generated/license_info.json" | ||||||
| import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; | import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" | ||||||
| import {FixImages} from "../Models/ThemeConfig/Conversion/FixImages"; | import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages" | ||||||
| import Svg from "../Svg"; | import Svg from "../Svg" | ||||||
| 
 | 
 | ||||||
| export default class DetermineLayout { | 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 |      * Gets the correct layout for this website | ||||||
|      */ |      */ | ||||||
|     public static async GetLayout(): Promise<LayoutConfig> { |     public static async GetLayout(): Promise<LayoutConfig> { | ||||||
| 
 |         const loadCustomThemeParam = QueryParameters.GetQueryParameter( | ||||||
|         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") |             "userlayout", | ||||||
|         const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data); |             "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")) { |         if (layoutFromBase64.startsWith("http")) { | ||||||
|             return await DetermineLayout.LoadRemoteTheme(layoutFromBase64) |             return await DetermineLayout.LoadRemoteTheme(layoutFromBase64) | ||||||
|  | @ -42,150 +44,164 @@ export default class DetermineLayout { | ||||||
| 
 | 
 | ||||||
|         let layoutId: string = undefined |         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 !== "") { |         if (path !== "theme.html" && path !== "") { | ||||||
|             layoutId = path; |             layoutId = path | ||||||
|             if (path.endsWith(".html")) { |             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()) |         return AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static LoadLayoutFromHash( |     public static LoadLayoutFromHash(userLayoutParam: UIEventSource<string>): LayoutConfig | null { | ||||||
|         userLayoutParam: UIEventSource<string> |         let hash = location.hash.substr(1) | ||||||
|     ): LayoutConfig | null { |         let json: any | ||||||
|         let hash = location.hash.substr(1); |  | ||||||
|         let json: any; |  | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             // layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
 |             // layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
 | ||||||
|             const dedicatedHashFromLocalStorage = LocalStorageSource.Get( |             const dedicatedHashFromLocalStorage = LocalStorageSource.Get( | ||||||
|                 "user-layout-" + userLayoutParam.data?.replace(" ", "_") |                 "user-layout-" + userLayoutParam.data?.replace(" ", "_") | ||||||
|             ); |             ) | ||||||
|             if (dedicatedHashFromLocalStorage.data?.length < 10) { |             if (dedicatedHashFromLocalStorage.data?.length < 10) { | ||||||
|                 dedicatedHashFromLocalStorage.setData(undefined); |                 dedicatedHashFromLocalStorage.setData(undefined) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const hashFromLocalStorage = LocalStorageSource.Get( |             const hashFromLocalStorage = LocalStorageSource.Get("last-loaded-user-layout") | ||||||
|                 "last-loaded-user-layout" |  | ||||||
|             ); |  | ||||||
|             if (hash.length < 10) { |             if (hash.length < 10) { | ||||||
|                 hash = |                 hash = dedicatedHashFromLocalStorage.data ?? hashFromLocalStorage.data | ||||||
|                     dedicatedHashFromLocalStorage.data ?? |  | ||||||
|                     hashFromLocalStorage.data; |  | ||||||
|             } else { |             } else { | ||||||
|                 console.log("Saving hash to local storage"); |                 console.log("Saving hash to local storage") | ||||||
|                 hashFromLocalStorage.setData(hash); |                 hashFromLocalStorage.setData(hash) | ||||||
|                 dedicatedHashFromLocalStorage.setData(hash); |                 dedicatedHashFromLocalStorage.setData(hash) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             try { |             try { | ||||||
|                 json = JSON.parse(atob(hash)); |                 json = JSON.parse(atob(hash)) | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 // We try to decode with lz-string
 |                 // We try to decode with lz-string
 | ||||||
|                 try { |                 try { | ||||||
|                     json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash))) |                     json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash))) | ||||||
|                 } catch (e) { |                 } catch (e) { | ||||||
|                     console.error(e) |                     console.error(e) | ||||||
|                     DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON")) |                     DetermineLayout.ShowErrorOnCustomTheme( | ||||||
|                     return null; |                         "Could not decode the hash", | ||||||
|  |                         new FixedUiElement("Not a valid (LZ-compressed) JSON") | ||||||
|  |                     ) | ||||||
|  |                     return null | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const layoutToUse = DetermineLayout.prepCustomTheme(json) |             const layoutToUse = DetermineLayout.prepCustomTheme(json) | ||||||
|             userLayoutParam.setData(layoutToUse.id); |             userLayoutParam.setData(layoutToUse.id) | ||||||
|             return layoutToUse |             return layoutToUse | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.error(e) |             console.error(e) | ||||||
|             if (hash === undefined || hash.length < 10) { |             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) |             this.ShowErrorOnCustomTheme("Could not parse the hash", new FixedUiElement(e), json) | ||||||
|             return null; |             return null | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static ShowErrorOnCustomTheme( |     public static ShowErrorOnCustomTheme( | ||||||
|         intro: string = "Error: could not parse the custom layout:", |         intro: string = "Error: could not parse the custom layout:", | ||||||
|         error: BaseUIElement, |         error: BaseUIElement, | ||||||
|         json?: any) { |         json?: any | ||||||
|  |     ) { | ||||||
|         new Combine([ |         new Combine([ | ||||||
|             intro, |             intro, | ||||||
|             error.SetClass("alert"), |             error.SetClass("alert"), | ||||||
|             new SubtleButton(Svg.back_svg(), |             new SubtleButton(Svg.back_svg(), "Go back to the theme overview", { | ||||||
|                 "Go back to the theme overview", |                 url: window.location.protocol + "//" + window.location.host + "/index.html", | ||||||
|                 {url: window.location.protocol + "//" + window.location.host + "/index.html", newTab: false}), |                 newTab: false, | ||||||
|             json !== undefined ? new SubtleButton(Svg.download_svg(),"Download the JSON file").onClick(() => { |             }), | ||||||
|                 Utils.offerContentsAsDownloadableFile(JSON.stringify(json, null, "  "), "theme_definition.json") |             json !== undefined | ||||||
|             }) : 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") |             .SetClass("flex flex-col clickable") | ||||||
|             .AttachTo("centermessage"); |             .AttachTo("centermessage") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static prepCustomTheme(json: any, sourceUrl?: string, forceId?: string): LayoutConfig { |     private static prepCustomTheme(json: any, sourceUrl?: string, forceId?: string): LayoutConfig { | ||||||
|          |         if (json.layers === undefined && json.tagRenderings !== undefined) { | ||||||
|         if(json.layers === undefined && json.tagRenderings !== undefined){ |             const iconTr = json.mapRendering.map((mr) => mr.icon).find((icon) => icon !== undefined) | ||||||
|             const iconTr = json.mapRendering.map(mr => mr.icon).find(icon => icon !== undefined) |  | ||||||
|             const icon = new TagRenderingConfig(iconTr).render.txt |             const icon = new TagRenderingConfig(iconTr).render.txt | ||||||
|             json = { |             json = { | ||||||
|                 id: json.id, |                 id: json.id, | ||||||
|                 description: json.description, |                 description: json.description, | ||||||
|                 descriptionTail: { |                 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, |                 icon, | ||||||
|                 title: json.name, |                 title: json.name, | ||||||
|                 layers: [json], |                 layers: [json], | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         const knownLayersDict = new Map<string, LayerConfigJson>() |         const knownLayersDict = new Map<string, LayerConfigJson>() | ||||||
|         for (const key in known_layers.layers) { |         for (const key in known_layers.layers) { | ||||||
|             const layer = known_layers.layers[key] |             const layer = known_layers.layers[key] | ||||||
|             knownLayersDict.set(layer.id,<LayerConfigJson> layer) |             knownLayersDict.set(layer.id, <LayerConfigJson>layer) | ||||||
|         } |         } | ||||||
|         const converState = { |         const converState = { | ||||||
|             tagRenderings: SharedTagRenderings.SharedTagRenderingJson, |             tagRenderings: SharedTagRenderings.SharedTagRenderingJson, | ||||||
|             sharedLayers: knownLayersDict, |             sharedLayers: knownLayersDict, | ||||||
|             publicLayers: new Set<string>() |             publicLayers: new Set<string>(), | ||||||
|         } |         } | ||||||
|         json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme") |         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 = new FixImages(DetermineLayout._knownImages).convertStrict( | ||||||
|         json.enableNoteImports = json.enableNoteImports ?? false; |             json, | ||||||
|  |             "While fixing the images" | ||||||
|  |         ) | ||||||
|  |         json.enableNoteImports = json.enableNoteImports ?? false | ||||||
|         json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme") |         json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme") | ||||||
|         console.log("The layoutconfig is ", json) |         console.log("The layoutconfig is ", json) | ||||||
|          | 
 | ||||||
|         json.id = forceId ?? json.id |         json.id = forceId ?? json.id | ||||||
|          | 
 | ||||||
|         return new LayoutConfig(json, false, { |         return new LayoutConfig(json, false, { | ||||||
|             definitionRaw: JSON.stringify(raw, null, "  "), |             definitionRaw: JSON.stringify(raw, null, "  "), | ||||||
|             definedAtUrl: sourceUrl |             definedAtUrl: sourceUrl, | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> { |     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>...`) |         new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`).AttachTo( | ||||||
|             .AttachTo("centermessage"); |             "centermessage" | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
| 
 |  | ||||||
|             let parsed = await Utils.downloadJson(link) |             let parsed = await Utils.downloadJson(link) | ||||||
|             try { |             try { | ||||||
|                 let forcedId = parsed.id |                 let forcedId = parsed.id | ||||||
|                 const url = new URL(link) |                 const url = new URL(link) | ||||||
|                 if(!(url.hostname === "localhost" || url.hostname === "127.0.0.1")){ |                 if (!(url.hostname === "localhost" || url.hostname === "127.0.0.1")) { | ||||||
|                     forcedId = link; |                     forcedId = link | ||||||
|                 } |                 } | ||||||
|                 console.log("Loaded remote link:", link) |                 console.log("Loaded remote link:", link) | ||||||
|                 return DetermineLayout.prepCustomTheme(parsed, link, forcedId); |                 return DetermineLayout.prepCustomTheme(parsed, link, forcedId) | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 console.error(e) |                 console.error(e) | ||||||
|                 DetermineLayout.ShowErrorOnCustomTheme( |                 DetermineLayout.ShowErrorOnCustomTheme( | ||||||
|  | @ -193,17 +209,15 @@ export default class DetermineLayout { | ||||||
|                     new FixedUiElement(e), |                     new FixedUiElement(e), | ||||||
|                     parsed |                     parsed | ||||||
|                 ) |                 ) | ||||||
|                 return null; |                 return null | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.error(e) |             console.error(e) | ||||||
|             DetermineLayout.ShowErrorOnCustomTheme( |             DetermineLayout.ShowErrorOnCustomTheme( | ||||||
|                 `<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`, |                 `<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`, | ||||||
|                 new FixedUiElement(e) |                 new FixedUiElement(e) | ||||||
|             ) |             ) | ||||||
|             return null; |             return null | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,20 +1,17 @@ | ||||||
| /** | /** | ||||||
|  * Keeps track of a dictionary 'elementID' -> UIEventSource<tags> |  * Keeps track of a dictionary 'elementID' -> UIEventSource<tags> | ||||||
|  */ |  */ | ||||||
| import {UIEventSource} from "./UIEventSource"; | import { UIEventSource } from "./UIEventSource" | ||||||
| import {GeoJSONObject} from "@turf/turf"; | import { GeoJSONObject } from "@turf/turf" | ||||||
| 
 | 
 | ||||||
| export class ElementStorage { | export class ElementStorage { | ||||||
|  |     public ContainingFeatures = new Map<string, any>() | ||||||
|  |     private _elements = new Map<string, UIEventSource<any>>() | ||||||
| 
 | 
 | ||||||
|     public ContainingFeatures = new Map<string, any>(); |     constructor() {} | ||||||
|     private _elements = new Map<string, UIEventSource<any>>(); |  | ||||||
| 
 |  | ||||||
|     constructor() { |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     addElementById(id: string, eventSource: UIEventSource<any>) { |     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 |      * Note: it will cleverly merge the tags, if needed | ||||||
|      */ |      */ | ||||||
|     addOrGetElement(feature: any): UIEventSource<any> { |     addOrGetElement(feature: any): UIEventSource<any> { | ||||||
|         const elementId = feature.properties.id; |         const elementId = feature.properties.id | ||||||
|         const newProperties = feature.properties; |         const newProperties = feature.properties | ||||||
| 
 | 
 | ||||||
|         const es = this.addOrGetById(elementId, newProperties) |         const es = this.addOrGetById(elementId, newProperties) | ||||||
| 
 | 
 | ||||||
|  | @ -33,91 +30,89 @@ export class ElementStorage { | ||||||
|         feature.properties = es.data |         feature.properties = es.data | ||||||
| 
 | 
 | ||||||
|         if (!this.ContainingFeatures.has(elementId)) { |         if (!this.ContainingFeatures.has(elementId)) { | ||||||
|             this.ContainingFeatures.set(elementId, feature); |             this.ContainingFeatures.set(elementId, feature) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return es; |         return es | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     getEventSourceById(elementId): UIEventSource<any> { |     getEventSourceById(elementId): UIEventSource<any> { | ||||||
|         if (elementId === undefined) { |         if (elementId === undefined) { | ||||||
|             return undefined; |             return undefined | ||||||
|         } |         } | ||||||
|         return this._elements.get(elementId); |         return this._elements.get(elementId) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     has(id) { |     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) { |         if (newId === undefined) { | ||||||
|             // We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
 |             // 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.data._deleted = "yes" | ||||||
|             element.ping(); |             element.ping() | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         if (oldId == newId) { |         if (oldId == newId) { | ||||||
|             return undefined; |             return undefined | ||||||
|         } |         } | ||||||
|         const element = this.getEventSourceById( oldId); |         const element = this.getEventSourceById(oldId) | ||||||
|         if (element === undefined) { |         if (element === undefined) { | ||||||
|             // Element to rewrite not found, probably a node or relation that is not rendered
 |             // Element to rewrite not found, probably a node or relation that is not rendered
 | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|         element.data.id = newId; |         element.data.id = newId | ||||||
|         this.addElementById(newId, element); |         this.addElementById(newId, element) | ||||||
|         this.ContainingFeatures.set(newId, this.ContainingFeatures.get( oldId)) |         this.ContainingFeatures.set(newId, this.ContainingFeatures.get(oldId)) | ||||||
|         element.ping(); |         element.ping() | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     private addOrGetById(elementId: string, newProperties: any): UIEventSource<any> { |     private addOrGetById(elementId: string, newProperties: any): UIEventSource<any> { | ||||||
|         if (!this._elements.has(elementId)) { |         if (!this._elements.has(elementId)) { | ||||||
|             const eventSource = new UIEventSource<any>(newProperties, "tags of " + elementId); |             const eventSource = new UIEventSource<any>(newProperties, "tags of " + elementId) | ||||||
|             this._elements.set(elementId, eventSource); |             this._elements.set(elementId, eventSource) | ||||||
|             return eventSource; |             return eventSource | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |         const es = this._elements.get(elementId) | ||||||
|         const es = this._elements.get(elementId); |  | ||||||
|         if (es.data == newProperties) { |         if (es.data == newProperties) { | ||||||
|             // Reference comparison gives the same object! we can just return the event source
 |             // 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
 |         // The element already exists
 | ||||||
|         // We use the new feature to overwrite all the properties in the already existing eventsource
 |         // We use the new feature to overwrite all the properties in the already existing eventsource
 | ||||||
|         const debug_msg = [] |         const debug_msg = [] | ||||||
|         let somethingChanged = false; |         let somethingChanged = false | ||||||
|         for (const k in newProperties) { |         for (const k in newProperties) { | ||||||
|             if (!newProperties.hasOwnProperty(k)) { |             if (!newProperties.hasOwnProperty(k)) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
|             const v = newProperties[k]; |             const v = newProperties[k] | ||||||
| 
 | 
 | ||||||
|             if (keptKeys[k] !== v) { |             if (keptKeys[k] !== v) { | ||||||
| 
 |  | ||||||
|                 if (v === undefined) { |                 if (v === undefined) { | ||||||
|                     // The new value is undefined; the tag might have been removed
 |                     // The new value is undefined; the tag might have been removed
 | ||||||
|                     // It might be a metatag as well
 |                     // It might be a metatag as well
 | ||||||
|                     // In the latter case, we do keep the tag!
 |                     // In the latter case, we do keep the tag!
 | ||||||
|                     if (!k.startsWith("_")) { |                     if (!k.startsWith("_")) { | ||||||
|                         delete keptKeys[k] |                         delete keptKeys[k] | ||||||
|                         debug_msg.push(("Erased " + k)) |                         debug_msg.push("Erased " + k) | ||||||
|                     } |                     } | ||||||
|                 } else { |                 } else { | ||||||
|                     keptKeys[k] = v; |                     keptKeys[k] = v | ||||||
|                     debug_msg.push(k + " --> " + v) |                     debug_msg.push(k + " --> " + v) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 somethingChanged = true; |                 somethingChanged = true | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if (somethingChanged) { |         if (somethingChanged) { | ||||||
|             es.ping(); |             es.ping() | ||||||
|         } |         } | ||||||
|         return es; |         return es | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import {GeoOperations} from "./GeoOperations"; | import { GeoOperations } from "./GeoOperations" | ||||||
| import Combine from "../UI/Base/Combine"; | import Combine from "../UI/Base/Combine" | ||||||
| import RelationsTracker from "./Osm/RelationsTracker"; | import RelationsTracker from "./Osm/RelationsTracker" | ||||||
| import BaseUIElement from "../UI/BaseUIElement"; | import BaseUIElement from "../UI/BaseUIElement" | ||||||
| import List from "../UI/Base/List"; | import List from "../UI/Base/List" | ||||||
| import Title from "../UI/Base/Title"; | import Title from "../UI/Base/Title" | ||||||
| import {BBox} from "./BBox"; | import { BBox } from "./BBox" | ||||||
| import {Feature, Geometry, MultiPolygon, Polygon} from "@turf/turf"; | import { Feature, Geometry, MultiPolygon, Polygon } from "@turf/turf" | ||||||
| 
 | 
 | ||||||
| export interface ExtraFuncParams { | export interface ExtraFuncParams { | ||||||
|     /** |     /** | ||||||
|  | @ -13,7 +13,7 @@ export interface ExtraFuncParams { | ||||||
|      * Note that more features then requested can be given back. |      * Note that more features then requested can be given back. | ||||||
|      * Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...] |      * 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 |     memberships: RelationsTracker | ||||||
|     getFeatureById: (id: string) => Feature<Geometry, { id: string }> |     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 |  * Describes a function that is added to a geojson object in order to calculate calculated tags | ||||||
|  */ |  */ | ||||||
| interface ExtraFunction { | interface ExtraFunction { | ||||||
|     readonly _name: string; |     readonly _name: string | ||||||
|     readonly _args: string[]; |     readonly _args: string[] | ||||||
|     readonly _doc: string; |     readonly _doc: string | ||||||
|     readonly _f: (params: ExtraFuncParams, feat: Feature<Geometry, any>) => any; |     readonly _f: (params: ExtraFuncParams, feat: Feature<Geometry, any>) => any | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class EnclosingFunc implements ExtraFunction { | class EnclosingFunc implements ExtraFunction { | ||||||
|     _name = "enclosingFeatures" |     _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}[]`", |         "The result is a list of features: `{feat: Polygon}[]`", | ||||||
|         "This function will never return the feature itself."].join("\n") |         "This function will never return the feature itself.", | ||||||
|     _args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"] |     ].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>) { |     _f(params: ExtraFuncParams, feat: Feature<Geometry, any>) { | ||||||
|         return (...layerIds: string[]) => { |         return (...layerIds: string[]) => { | ||||||
|  | @ -45,10 +49,10 @@ class EnclosingFunc implements ExtraFunction { | ||||||
|             for (const layerId of layerIds) { |             for (const layerId of layerIds) { | ||||||
|                 const otherFeaturess = params.getFeaturesWithin(layerId, bbox) |                 const otherFeaturess = params.getFeaturesWithin(layerId, bbox) | ||||||
|                 if (otherFeaturess === undefined) { |                 if (otherFeaturess === undefined) { | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
|                 if (otherFeaturess.length === 0) { |                 if (otherFeaturess.length === 0) { | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
|                 for (const otherFeatures of otherFeaturess) { |                 for (const otherFeatures of otherFeaturess) { | ||||||
|                     for (const otherFeature of otherFeatures) { |                     for (const otherFeature of otherFeatures) { | ||||||
|  | @ -56,26 +60,33 @@ class EnclosingFunc implements ExtraFunction { | ||||||
|                             continue |                             continue | ||||||
|                         } |                         } | ||||||
|                         seenIds.add(otherFeature.properties.id) |                         seenIds.add(otherFeature.properties.id) | ||||||
|                         if (otherFeature.geometry.type !== "Polygon" && otherFeature.geometry.type !== "MultiPolygon") { |                         if ( | ||||||
|                             continue; |                             otherFeature.geometry.type !== "Polygon" && | ||||||
|  |                             otherFeature.geometry.type !== "MultiPolygon" | ||||||
|  |                         ) { | ||||||
|  |                             continue | ||||||
|                         } |                         } | ||||||
|                         if (GeoOperations.completelyWithin(feat, <Feature<Polygon | MultiPolygon, any>>otherFeature)) { |                         if ( | ||||||
|                             result.push({feat: otherFeature}) |                             GeoOperations.completelyWithin( | ||||||
|  |                                 feat, | ||||||
|  |                                 <Feature<Polygon | MultiPolygon, any>>otherFeature | ||||||
|  |                             ) | ||||||
|  |                         ) { | ||||||
|  |                             result.push({ feat: otherFeature }) | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return result; |             return result | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class OverlapFunc implements ExtraFunction { | class OverlapFunc implements ExtraFunction { | ||||||
| 
 |     _name = "overlapWith" | ||||||
| 
 |     _doc = [ | ||||||
|     _name = "overlapWith"; |         "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.", | ||||||
|     _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.", |         "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.", |         "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')`", |         "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") |     ].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) { |     _f(params, feat) { | ||||||
|         return (...layerIds: string[]) => { |         return (...layerIds: string[]) => { | ||||||
|             const result: { feat: any, overlap: number }[] = [] |             const result: { feat: any; overlap: number }[] = [] | ||||||
|             const seenIds = new Set<string>() |             const seenIds = new Set<string>() | ||||||
|             const bbox = BBox.get(feat) |             const bbox = BBox.get(feat) | ||||||
|             for (const layerId of layerIds) { |             for (const layerId of layerIds) { | ||||||
|                 const otherFeaturess = params.getFeaturesWithin(layerId, bbox) |                 const otherFeaturess = params.getFeaturesWithin(layerId, bbox) | ||||||
|                 if (otherFeaturess === undefined) { |                 if (otherFeaturess === undefined) { | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
|                 if (otherFeaturess.length === 0) { |                 if (otherFeaturess.length === 0) { | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
|                 for (const otherFeatures of otherFeaturess) { |                 for (const otherFeatures of otherFeaturess) { | ||||||
|                     const overlap = GeoOperations.calculateOverlap(feat, otherFeatures) |                     const overlap = GeoOperations.calculateOverlap(feat, otherFeatures) | ||||||
|                     for (const overlappingFeature of overlap) { |                     for (const overlappingFeature of overlap) { | ||||||
|                         if(seenIds.has(overlappingFeature.feat.properties.id)){ |                         if (seenIds.has(overlappingFeature.feat.properties.id)) { | ||||||
|                             continue |                             continue | ||||||
|                         } |                         } | ||||||
|                         seenIds.add(overlappingFeature.feat.properties.id) |                         seenIds.add(overlappingFeature.feat.properties.id) | ||||||
|  | @ -113,105 +126,113 @@ class OverlapFunc implements ExtraFunction { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             result.sort((a, b) => b.overlap - a.overlap) |             result.sort((a, b) => b.overlap - a.overlap) | ||||||
|             return result; |             return result | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| class IntersectionFunc implements ExtraFunction { | class IntersectionFunc implements ExtraFunction { | ||||||
| 
 |     _name = "intersectionsWith" | ||||||
| 
 |     _doc = | ||||||
|     _name = "intersectionsWith"; |         "Gives the intersection points with selected features. Only works with (Multi)Polygons and LineStrings.\n\n" + | ||||||
|     _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" + |         "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" + |         "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." |         "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) { |     _f(params: ExtraFuncParams, feat) { | ||||||
|         return (...layerIds: string[]) => { |         return (...layerIds: string[]) => { | ||||||
|             const result: { feat: any, intersections: [number, number][] }[] = [] |             const result: { feat: any; intersections: [number, number][] }[] = [] | ||||||
| 
 | 
 | ||||||
|             const bbox = BBox.get(feat) |             const bbox = BBox.get(feat) | ||||||
| 
 | 
 | ||||||
|             for (const layerId of layerIds) { |             for (const layerId of layerIds) { | ||||||
|                 const otherLayers = params.getFeaturesWithin(layerId, bbox) |                 const otherLayers = params.getFeaturesWithin(layerId, bbox) | ||||||
|                 if (otherLayers === undefined) { |                 if (otherLayers === undefined) { | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
|                 if (otherLayers.length === 0) { |                 if (otherLayers.length === 0) { | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
|                 for (const tile of otherLayers) { |                 for (const tile of otherLayers) { | ||||||
|                     for (const otherFeature of tile) { |                     for (const otherFeature of tile) { | ||||||
| 
 |  | ||||||
|                         const intersections = GeoOperations.LineIntersections(feat, otherFeature) |                         const intersections = GeoOperations.LineIntersections(feat, otherFeature) | ||||||
|                         if (intersections.length === 0) { |                         if (intersections.length === 0) { | ||||||
|                             continue |                             continue | ||||||
|                         } |                         } | ||||||
|                         result.push({feat: otherFeature, intersections}) |                         result.push({ feat: otherFeature, intersections }) | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return result; |             return result | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| class DistanceToFunc implements ExtraFunction { | class DistanceToFunc implements ExtraFunction { | ||||||
| 
 |     _name = "distanceTo" | ||||||
|     _name = "distanceTo"; |     _doc = | ||||||
|     _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"; |         "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"] |     _args = ["feature OR featureID OR longitude", "undefined OR latitude"] | ||||||
| 
 | 
 | ||||||
|     _f(featuresPerLayer, feature) { |     _f(featuresPerLayer, feature) { | ||||||
|         return (arg0, lat) => { |         return (arg0, lat) => { | ||||||
|             if (arg0 === undefined) { |             if (arg0 === undefined) { | ||||||
|                 return undefined; |                 return undefined | ||||||
|             } |             } | ||||||
|             if (typeof arg0 === "number") { |             if (typeof arg0 === "number") { | ||||||
|                 // Feature._lon and ._lat is conveniently place by one of the other metatags
 |                 // 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") { |             if (typeof arg0 === "string") { | ||||||
|                 // This is an identifier
 |                 // This is an identifier
 | ||||||
|                 const feature = featuresPerLayer.getFeatureById(arg0) |                 const feature = featuresPerLayer.getFeatureById(arg0) | ||||||
|                 if (feature === undefined) { |                 if (feature === undefined) { | ||||||
|                     return undefined; |                     return undefined | ||||||
|                 } |                 } | ||||||
|                 arg0 = feature; |                 arg0 = feature | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // arg0 is probably a geojsonfeature
 |             // 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 { | class ClosestObjectFunc implements ExtraFunction { | ||||||
|     _name = "closest" |     _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"] |     _args = ["list of features or a layer name or '*' to get all features"] | ||||||
| 
 | 
 | ||||||
|     _f(params, feature) { |     _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 { | class ClosestNObjectFunc implements ExtraFunction { | ||||||
|     _name = "closestn" |     _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" + |         "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)" |         "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. |      * Gets the closes N features, sorted by ascending distance. | ||||||
|  | @ -223,45 +244,61 @@ class ClosestNObjectFunc implements ExtraFunction { | ||||||
|      * @constructor |      * @constructor | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     static GetClosestNFeatures(params: ExtraFuncParams, |     static GetClosestNFeatures( | ||||||
|                                feature: any, |         params: ExtraFuncParams, | ||||||
|                                features: string | any[], |         feature: any, | ||||||
|                                options?: { maxFeatures?: number, uniqueTag?: string | undefined, maxDistance?: number }): { feat: any, distance: number }[] { |         features: string | any[], | ||||||
|  |         options?: { maxFeatures?: number; uniqueTag?: string | undefined; maxDistance?: number } | ||||||
|  |     ): { feat: any; distance: number }[] { | ||||||
|         const maxFeatures = options?.maxFeatures ?? 1 |         const maxFeatures = options?.maxFeatures ?? 1 | ||||||
|         const maxDistance = options?.maxDistance ?? 500 |         const maxDistance = options?.maxDistance ?? 500 | ||||||
|         const uniqueTag: string | undefined = options?.uniqueTag |         const uniqueTag: string | undefined = options?.uniqueTag | ||||||
|         if (typeof features === "string") { |         if (typeof features === "string") { | ||||||
|             const name = features |             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)) |             features = params.getFeaturesWithin(name, new BBox(bbox.geometry.coordinates)) | ||||||
|         } else { |         } else { | ||||||
|             features = [features] |             features = [features] | ||||||
|         } |         } | ||||||
|         if (features === undefined) { |         if (features === undefined) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const selfCenter = GeoOperations.centerpointCoordinates(feature) |         const selfCenter = GeoOperations.centerpointCoordinates(feature) | ||||||
|         let closestFeatures: { feat: any, distance: number }[] = []; |         let closestFeatures: { feat: any; distance: number }[] = [] | ||||||
| 
 | 
 | ||||||
|         for (const featureList of features) { |         for (const featureList of features) { | ||||||
|             // Features is provided by 'getFeaturesWithin' which returns a list of lists of features, hence the double loop here
 |             // Features is provided by 'getFeaturesWithin' which returns a list of lists of features, hence the double loop here
 | ||||||
|             for (const otherFeature of featureList) { |             for (const otherFeature of featureList) { | ||||||
| 
 |                 if ( | ||||||
|                 if (otherFeature === feature || otherFeature.properties.id === feature.properties.id) { |                     otherFeature === feature || | ||||||
|                     continue; // We ignore self
 |                     otherFeature.properties.id === feature.properties.id | ||||||
|  |                 ) { | ||||||
|  |                     continue // We ignore self
 | ||||||
|                 } |                 } | ||||||
|                 const distance = GeoOperations.distanceBetween( |                 const distance = GeoOperations.distanceBetween( | ||||||
|                     GeoOperations.centerpointCoordinates(otherFeature), |                     GeoOperations.centerpointCoordinates(otherFeature), | ||||||
|                     selfCenter |                     selfCenter | ||||||
|                 ) |                 ) | ||||||
|                 if (distance === undefined || distance === null || isNaN(distance)) { |                 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!" |                     throw "Undefined distance!" | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (distance === 0) { |                 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) { |                 if (distance > maxDistance) { | ||||||
|  | @ -272,13 +309,15 @@ class ClosestNObjectFunc implements ExtraFunction { | ||||||
|                     // This is the first matching feature we find - always add it
 |                     // This is the first matching feature we find - always add it
 | ||||||
|                     closestFeatures.push({ |                     closestFeatures.push({ | ||||||
|                         feat: otherFeature, |                         feat: otherFeature, | ||||||
|                         distance: distance |                         distance: distance, | ||||||
|                     }) |                     }) | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
| 
 |                 if ( | ||||||
|                 if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { |                     closestFeatures.length >= maxFeatures && | ||||||
|  |                     closestFeatures[maxFeatures - 1].distance < distance | ||||||
|  |                 ) { | ||||||
|                     // The last feature of the list (and thus the furthest away is still closer
 |                     // 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!
 |                     // No use for checking, as we already have plenty of features!
 | ||||||
|                     continue |                     continue | ||||||
|  | @ -286,11 +325,13 @@ class ClosestNObjectFunc implements ExtraFunction { | ||||||
| 
 | 
 | ||||||
|                 let targetIndex = closestFeatures.length |                 let targetIndex = closestFeatures.length | ||||||
|                 for (let i = 0; i < closestFeatures.length; i++) { |                 for (let i = 0; i < closestFeatures.length; i++) { | ||||||
|                     const closestFeature = closestFeatures[i]; |                     const closestFeature = closestFeatures[i] | ||||||
| 
 | 
 | ||||||
|                     if (uniqueTag !== undefined) { |                     if (uniqueTag !== undefined) { | ||||||
|                         const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined && |                         const uniqueTagsMatch = | ||||||
|                             closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag] |                             otherFeature.properties[uniqueTag] !== undefined && | ||||||
|  |                             closestFeature.feat.properties[uniqueTag] === | ||||||
|  |                                 otherFeature.properties[uniqueTag] | ||||||
|                         if (uniqueTagsMatch) { |                         if (uniqueTagsMatch) { | ||||||
|                             targetIndex = -1 |                             targetIndex = -1 | ||||||
|                             if (closestFeature.distance > distance) { |                             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')
 |                                 // 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
 |                                 // AT this point, we have found a closer segment with the same, identical tag
 | ||||||
|                                 // so we replace directly
 |                                 // 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) { |                 if (targetIndex == -1) { | ||||||
|                     continue; // value is already swapped by the unique tag
 |                     continue // value is already swapped by the unique tag
 | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (targetIndex < maxFeatures) { |                 if (targetIndex < maxFeatures) { | ||||||
|                     // insert and drop one
 |                     // insert and drop one
 | ||||||
|                     closestFeatures.splice(targetIndex, 0, { |                     closestFeatures.splice(targetIndex, 0, { | ||||||
|                         feat: otherFeature, |                         feat: otherFeature, | ||||||
|                         distance: distance |                         distance: distance, | ||||||
|                     }) |                     }) | ||||||
|                     if (closestFeatures.length >= maxFeatures) { |                     if (closestFeatures.length >= maxFeatures) { | ||||||
|                         closestFeatures.splice(maxFeatures, 1) |                         closestFeatures.splice(maxFeatures, 1) | ||||||
|  | @ -337,19 +378,15 @@ class ClosestNObjectFunc implements ExtraFunction { | ||||||
|                     // Overwrite the last element
 |                     // Overwrite the last element
 | ||||||
|                     closestFeatures[targetIndex] = { |                     closestFeatures[targetIndex] = { | ||||||
|                         feat: otherFeature, |                         feat: otherFeature, | ||||||
|                         distance: distance |                         distance: distance, | ||||||
|                     } |                     } | ||||||
| 
 |  | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return closestFeatures; |         return closestFeatures | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     _f(params, feature) { |     _f(params, feature) { | ||||||
| 
 |  | ||||||
|         return (features, amount, uniqueTag, maxDistanceInMeters) => { |         return (features, amount, uniqueTag, maxDistanceInMeters) => { | ||||||
|             let distance: number = Number(maxDistanceInMeters) |             let distance: number = Number(maxDistanceInMeters) | ||||||
|             if (isNaN(distance)) { |             if (isNaN(distance)) { | ||||||
|  | @ -358,60 +395,54 @@ class ClosestNObjectFunc implements ExtraFunction { | ||||||
|             return ClosestNObjectFunc.GetClosestNFeatures(params, feature, features, { |             return ClosestNObjectFunc.GetClosestNFeatures(params, feature, features, { | ||||||
|                 maxFeatures: Number(amount), |                 maxFeatures: Number(amount), | ||||||
|                 uniqueTag: uniqueTag, |                 uniqueTag: uniqueTag, | ||||||
|                 maxDistance: distance |                 maxDistance: distance, | ||||||
|             }); |             }) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| class Memberships implements ExtraFunction { | class Memberships implements ExtraFunction { | ||||||
|     _name = "memberships" |     _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" + |         "\n\n" + | ||||||
|         "For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`" |         "For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`" | ||||||
|     _args = [] |     _args = [] | ||||||
| 
 | 
 | ||||||
|     _f(params, feat) { |     _f(params, feat) { | ||||||
|         return () => |         return () => params.memberships.knownRelations.data.get(feat.properties.id) ?? [] | ||||||
|             params.memberships.knownRelations.data.get(feat.properties.id) ?? [] |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| class GetParsed implements ExtraFunction { | class GetParsed implements ExtraFunction { | ||||||
|     _name = "get" |     _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"] |     _args = ["key"] | ||||||
| 
 | 
 | ||||||
|     _f(params, feat) { |     _f(params, feat) { | ||||||
|         return key => { |         return (key) => { | ||||||
|             const value = feat.properties[key] |             const value = feat.properties[key] | ||||||
|             if (value === undefined) { |             if (value === undefined) { | ||||||
|                 return undefined; |                 return undefined | ||||||
|             } |             } | ||||||
|             try { |             try { | ||||||
|                 const parsed = JSON.parse(value) |                 const parsed = JSON.parse(value) | ||||||
|                 if (parsed === null) { |                 if (parsed === null) { | ||||||
|                     return undefined; |                     return undefined | ||||||
|                 } |                 } | ||||||
|                 return parsed; |                 return parsed | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 console.warn("Could not parse property " + key + " due to: " + e + ", the value is " + value) |                 console.warn( | ||||||
|                 return undefined; |                     "Could not parse property " + key + " due to: " + e + ", the value is " + value | ||||||
|  |                 ) | ||||||
|  |                 return undefined | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| export class ExtraFunctions { | export class ExtraFunctions { | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     static readonly intro = new Combine([ |     static readonly intro = new Combine([ | ||||||
|         new Title("Calculating tags with Javascript", 2), |         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.", |         "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([ |         new List([ | ||||||
|             "DO NOT DO THIS AS BEGINNER", |             "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", |             "**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.:", |         "To enable this feature,  add a field `calculatedTags` in the layer object, e.g.:", | ||||||
|         "````", |         "````", | ||||||
|         "\"calculatedTags\": [", |         '"calculatedTags": [', | ||||||
|         "    \"_someKey=javascript-expression\",", |         '    "_someKey=javascript-expression",', | ||||||
|         "    \"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator\",", |         '    "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",', | ||||||
|         "    \"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ", |         "    \"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ", | ||||||
|         "  ]", |         "  ]", | ||||||
|         "````", |         "````", | ||||||
|  | @ -436,11 +467,12 @@ export class ExtraFunctions { | ||||||
| 
 | 
 | ||||||
|         new List([ |         new List([ | ||||||
|             "`area` contains the surface area (in square meters) of the object", |             "`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:" |         "Some advanced functions are available on **feat** as well:", | ||||||
|     ]).SetClass("flex-col").AsMarkdown(); |     ]) | ||||||
| 
 |         .SetClass("flex-col") | ||||||
|  |         .AsMarkdown() | ||||||
| 
 | 
 | ||||||
|     private static readonly allFuncs: ExtraFunction[] = [ |     private static readonly allFuncs: ExtraFunction[] = [ | ||||||
|         new DistanceToFunc(), |         new DistanceToFunc(), | ||||||
|  | @ -450,8 +482,8 @@ export class ExtraFunctions { | ||||||
|         new ClosestObjectFunc(), |         new ClosestObjectFunc(), | ||||||
|         new ClosestNObjectFunc(), |         new ClosestNObjectFunc(), | ||||||
|         new Memberships(), |         new Memberships(), | ||||||
|         new GetParsed() |         new GetParsed(), | ||||||
|     ]; |     ] | ||||||
| 
 | 
 | ||||||
|     public static FullPatchFeature(params: ExtraFuncParams, feature) { |     public static FullPatchFeature(params: ExtraFuncParams, feature) { | ||||||
|         if (feature._is_patched) { |         if (feature._is_patched) { | ||||||
|  | @ -464,20 +496,15 @@ export class ExtraFunctions { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static HelpText(): BaseUIElement { |     public static HelpText(): BaseUIElement { | ||||||
| 
 |  | ||||||
|         const elems = [] |         const elems = [] | ||||||
|         for (const func of ExtraFunctions.allFuncs) { |         for (const func of ExtraFunctions.allFuncs) { | ||||||
|             elems.push(new Title(func._name, 3), |             elems.push(new Title(func._name, 3), func._doc, new List(func._args ?? [], true)) | ||||||
|                 func._doc, |  | ||||||
|                 new List(func._args ?? [], true)) |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return new Combine([ |         return new Combine([ | ||||||
|             ExtraFunctions.intro, |             ExtraFunctions.intro, | ||||||
|             new List(ExtraFunctions.allFuncs.map(func => `[${func._name}](#${func._name})`)), |             new List(ExtraFunctions.allFuncs.map((func) => `[${func._name}](#${func._name})`)), | ||||||
|             ...elems |             ...elems, | ||||||
|         ]); |         ]) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,26 +1,30 @@ | ||||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||||
| import MetaTagging from "../../MetaTagging"; | import MetaTagging from "../../MetaTagging" | ||||||
| import {ElementStorage} from "../../ElementStorage"; | import { ElementStorage } from "../../ElementStorage" | ||||||
| import {ExtraFuncParams} from "../../ExtraFunctions"; | import { ExtraFuncParams } from "../../ExtraFunctions" | ||||||
| import FeaturePipeline from "../FeaturePipeline"; | import FeaturePipeline from "../FeaturePipeline" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import { UIEventSource } from "../../UIEventSource" | ||||||
| 
 | 
 | ||||||
| /**** | /**** | ||||||
|  * Concerned with the logic of updating the right layer at the right time |  * Concerned with the logic of updating the right layer at the right time | ||||||
|  */ |  */ | ||||||
| class MetatagUpdater { | class MetatagUpdater { | ||||||
|     public readonly neededLayerBboxes = new Map<string /*layerId*/, BBox>() |     public readonly neededLayerBboxes = new Map<string /*layerId*/, BBox>() | ||||||
|     private source: FeatureSourceForLayer & Tiled; |     private source: FeatureSourceForLayer & Tiled | ||||||
|     private readonly params: ExtraFuncParams |     private readonly params: ExtraFuncParams | ||||||
|     private state: { allElements?: ElementStorage }; |     private state: { allElements?: ElementStorage } | ||||||
| 
 | 
 | ||||||
|     private readonly isDirty = new UIEventSource(false) |     private readonly isDirty = new UIEventSource(false) | ||||||
| 
 | 
 | ||||||
|     constructor(source: FeatureSourceForLayer & Tiled, state: { allElements?: ElementStorage }, featurePipeline: FeaturePipeline) { |     constructor( | ||||||
|         this.state = state; |         source: FeatureSourceForLayer & Tiled, | ||||||
|         this.source = source; |         state: { allElements?: ElementStorage }, | ||||||
|         const self = this; |         featurePipeline: FeaturePipeline | ||||||
|  |     ) { | ||||||
|  |         this.state = state | ||||||
|  |         this.source = source | ||||||
|  |         const self = this | ||||||
|         this.params = { |         this.params = { | ||||||
|             getFeatureById(id) { |             getFeatureById(id) { | ||||||
|                 return state.allElements.ContainingFeatures.get(id) |                 return state.allElements.ContainingFeatures.get(id) | ||||||
|  | @ -29,21 +33,20 @@ class MetatagUpdater { | ||||||
|                 // We keep track of the BBOX that this source needs
 |                 // We keep track of the BBOX that this source needs
 | ||||||
|                 let oldBbox: BBox = self.neededLayerBboxes.get(layerId) |                 let oldBbox: BBox = self.neededLayerBboxes.get(layerId) | ||||||
|                 if (oldBbox === undefined) { |                 if (oldBbox === undefined) { | ||||||
|                     self.neededLayerBboxes.set(layerId, bbox); |                     self.neededLayerBboxes.set(layerId, bbox) | ||||||
|                 } else if (!bbox.isContainedIn(oldBbox)) { |                 } else if (!bbox.isContainedIn(oldBbox)) { | ||||||
|                     self.neededLayerBboxes.set(layerId, oldBbox.unionWith(bbox)) |                     self.neededLayerBboxes.set(layerId, oldBbox.unionWith(bbox)) | ||||||
|                 } |                 } | ||||||
|                 return featurePipeline.GetFeaturesWithin(layerId, 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) { |             if (dirty) { | ||||||
|                 self.updateMetaTags() |                 self.updateMetaTags() | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|         this.source.features.addCallbackAndRunD(_ => self.isDirty.setData(true)) |         this.source.features.addCallbackAndRunD((_) => self.isDirty.setData(true)) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public requestUpdate() { |     public requestUpdate() { | ||||||
|  | @ -57,56 +60,58 @@ class MetatagUpdater { | ||||||
|             this.isDirty.setData(false) |             this.isDirty.setData(false) | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         MetaTagging.addMetatags( |         MetaTagging.addMetatags(features, this.params, this.source.layer.layerDef, this.state) | ||||||
|             features, |  | ||||||
|             this.params, |  | ||||||
|             this.source.layer.layerDef, |  | ||||||
|             this.state) |  | ||||||
|         this.isDirty.setData(false) |         this.isDirty.setData(false) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default class MetaTagRecalculator { | export default class MetaTagRecalculator { | ||||||
|     private _state: { |     private _state: { | ||||||
|         allElements?: ElementStorage |         allElements?: ElementStorage | ||||||
|     }; |     } | ||||||
|     private _featurePipeline: FeaturePipeline; |     private _featurePipeline: FeaturePipeline | ||||||
|     private readonly _alreadyRegistered: Set<FeatureSourceForLayer & Tiled> = new Set<FeatureSourceForLayer & Tiled>() |     private readonly _alreadyRegistered: Set<FeatureSourceForLayer & Tiled> = new Set< | ||||||
|  |         FeatureSourceForLayer & Tiled | ||||||
|  |     >() | ||||||
|     private readonly _notifiers: MetatagUpdater[] = [] |     private readonly _notifiers: MetatagUpdater[] = [] | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * The meta tag recalculator receives tiles of layers via the 'registerSource'-function. |      * 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 |      * 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) { |     constructor( | ||||||
|         this._featurePipeline = featurePipeline; |         state: { allElements?: ElementStorage; currentView: FeatureSourceForLayer & Tiled }, | ||||||
|         this._state = state; |         featurePipeline: FeaturePipeline | ||||||
|          |     ) { | ||||||
|         if(state.currentView !== undefined){ |         this._featurePipeline = featurePipeline | ||||||
|         const currentViewUpdater = new MetatagUpdater(state.currentView, this._state, this._featurePipeline) |         this._state = state | ||||||
|         this._alreadyRegistered.add(state.currentView) |  | ||||||
|         this._notifiers.push(currentViewUpdater) |  | ||||||
|         state.currentView.features.addCallback(_ => { |  | ||||||
|             console.debug("Requesting an update for currentView") |  | ||||||
|             currentViewUpdater.updateMetaTags(); |  | ||||||
|         }) |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|  |         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((_) => { | ||||||
|  |                 console.debug("Requesting an update for currentView") | ||||||
|  |                 currentViewUpdater.updateMetaTags() | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public registerSource(source: FeatureSourceForLayer & Tiled, recalculateOnEveryChange = false) { |     public registerSource(source: FeatureSourceForLayer & Tiled, recalculateOnEveryChange = false) { | ||||||
|         if (source === undefined) { |         if (source === undefined) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         if (this._alreadyRegistered.has(source)) { |         if (this._alreadyRegistered.has(source)) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         this._alreadyRegistered.add(source) |         this._alreadyRegistered.add(source) | ||||||
|         this._notifiers.push(new MetatagUpdater(source, this._state, this._featurePipeline)) |         this._notifiers.push(new MetatagUpdater(source, this._state, this._featurePipeline)) | ||||||
|         const self = this; |         const self = this | ||||||
|         source.features.addCallbackAndRunD(_ => { |         source.features.addCallbackAndRunD((_) => { | ||||||
|             const layerName = source.layer.layerDef.id |             const layerName = source.layer.layerDef.id | ||||||
|             for (const updater of self._notifiers) { |             for (const updater of self._notifiers) { | ||||||
|                 const neededBbox = updater.neededLayerBboxes.get(layerName) |                 const neededBbox = updater.neededLayerBboxes.get(layerName) | ||||||
|  | @ -118,7 +123,5 @@ export default class MetaTagRecalculator { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,22 +1,21 @@ | ||||||
| import FeatureSource from "../FeatureSource"; | import FeatureSource from "../FeatureSource" | ||||||
| import {Store} from "../../UIEventSource"; | import { Store } from "../../UIEventSource" | ||||||
| import {ElementStorage} from "../../ElementStorage"; | import { ElementStorage } from "../../ElementStorage" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Makes sure that every feature is added to the ElementsStorage, so that the tags-eventsource can be retrieved |  * Makes sure that every feature is added to the ElementsStorage, so that the tags-eventsource can be retrieved | ||||||
|  */ |  */ | ||||||
| export default class RegisteringAllFromFeatureSourceActor { | export default class RegisteringAllFromFeatureSourceActor { | ||||||
|     public readonly features: Store<{ feature: any; freshness: Date }[]>; |     public readonly features: Store<{ feature: any; freshness: Date }[]> | ||||||
|     public readonly name; |     public readonly name | ||||||
| 
 | 
 | ||||||
|     constructor(source: FeatureSource, allElements: ElementStorage) { |     constructor(source: FeatureSource, allElements: ElementStorage) { | ||||||
|         this.features = source.features; |         this.features = source.features | ||||||
|         this.name = "RegisteringSource of " + source.name; |         this.name = "RegisteringSource of " + source.name | ||||||
|         this.features.addCallbackAndRunD(features => { |         this.features.addCallbackAndRunD((features) => { | ||||||
|             for (const feature of features) { |             for (const feature of features) { | ||||||
|                 allElements.addOrGetElement(feature.feature) |                 allElements.addOrGetElement(feature.feature) | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,12 +1,12 @@ | ||||||
| import FeatureSource, {Tiled} from "../FeatureSource"; | import FeatureSource, { Tiled } from "../FeatureSource" | ||||||
| import {Tiles} from "../../../Models/TileRange"; | import { Tiles } from "../../../Models/TileRange" | ||||||
| import {IdbLocalStorage} from "../../Web/IdbLocalStorage"; | import { IdbLocalStorage } from "../../Web/IdbLocalStorage" | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import { UIEventSource } from "../../UIEventSource" | ||||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| import SimpleFeatureSource from "../Sources/SimpleFeatureSource"; | import SimpleFeatureSource from "../Sources/SimpleFeatureSource" | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import Loc from "../../../Models/Loc"; | import Loc from "../../../Models/Loc" | ||||||
| 
 | 
 | ||||||
| /*** | /*** | ||||||
|  * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run |  * 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 { | export default class SaveTileToLocalStorageActor { | ||||||
|     private readonly visitedTiles: UIEventSource<Map<number, Date>> |     private readonly visitedTiles: UIEventSource<Map<number, Date>> | ||||||
|     private readonly _layer: LayerConfig; |     private readonly _layer: LayerConfig | ||||||
|     private readonly _flayer: FilteredLayer |     private readonly _flayer: FilteredLayer | ||||||
|     private readonly initializeTime = new Date() |     private readonly initializeTime = new Date() | ||||||
| 
 | 
 | ||||||
|     constructor(layer: FilteredLayer) { |     constructor(layer: FilteredLayer) { | ||||||
|         this._flayer = layer |         this._flayer = layer | ||||||
|         this._layer = layer.layerDef |         this._layer = layer.layerDef | ||||||
|         this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, |         this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, { | ||||||
|             {defaultValue: new Map<number, Date>(),}) |             defaultValue: new Map<number, Date>(), | ||||||
|         this.visitedTiles.stabilized(100).addCallbackAndRunD(tiles => { |         }) | ||||||
|  |         this.visitedTiles.stabilized(100).addCallbackAndRunD((tiles) => { | ||||||
|             for (const key of Array.from(tiles.keys())) { |             for (const key of Array.from(tiles.keys())) { | ||||||
|                 const tileFreshness = tiles.get(key) |                 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) { |                 if (toOld) { | ||||||
|                     // Purge this tile
 |                     // Purge this tile
 | ||||||
|                     this.SetIdb(key, undefined) |                     this.SetIdb(key, undefined) | ||||||
|  | @ -37,27 +40,28 @@ export default class SaveTileToLocalStorageActor { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             this.visitedTiles.ping() |             this.visitedTiles.ping() | ||||||
|             return true; |             return true | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |     public LoadTilesFromDisk( | ||||||
|     public LoadTilesFromDisk(currentBounds: UIEventSource<BBox>, location: UIEventSource<Loc>, |         currentBounds: UIEventSource<BBox>, | ||||||
|                              registerFreshness: (tileId: number, freshness: Date) => void, |         location: UIEventSource<Loc>, | ||||||
|                              registerTile: ((src: FeatureSource & Tiled) => void)) { |         registerFreshness: (tileId: number, freshness: Date) => void, | ||||||
|         const self = this; |         registerTile: (src: FeatureSource & Tiled) => void | ||||||
|  |     ) { | ||||||
|  |         const self = this | ||||||
|         const loadedTiles = new Set<number>() |         const loadedTiles = new Set<number>() | ||||||
|         this.visitedTiles.addCallbackD(tiles => { |         this.visitedTiles.addCallbackD((tiles) => { | ||||||
|             if (tiles.size === 0) { |             if (tiles.size === 0) { | ||||||
|                 // We don't do anything yet as probably not yet loaded from disk
 |                 // We don't do anything yet as probably not yet loaded from disk
 | ||||||
|                 // We'll unregister later on
 |                 // We'll unregister later on
 | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             currentBounds.addCallbackAndRunD(bbox => { |             currentBounds.addCallbackAndRunD((bbox) => { | ||||||
| 
 |  | ||||||
|                 if (self._layer.minzoomVisible > location.data.zoom) { |                 if (self._layer.minzoomVisible > location.data.zoom) { | ||||||
|                     // Not enough zoom
 |                     // Not enough zoom
 | ||||||
|                     return; |                     return | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // Iterate over all available keys in the local storage, check which are needed and fresh enough
 |                 // 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) |                     registerFreshness(key, tileFreshness) | ||||||
|                     const tileBbox = BBox.fromTileIndex(key) |                     const tileBbox = BBox.fromTileIndex(key) | ||||||
|                     if (!bbox.overlapsWith(tileBbox)) { |                     if (!bbox.overlapsWith(tileBbox)) { | ||||||
|                         continue; |                         continue | ||||||
|                     } |                     } | ||||||
|                     if (loadedTiles.has(key)) { |                     if (loadedTiles.has(key)) { | ||||||
|                         // Already loaded earlier
 |                         // Already loaded earlier
 | ||||||
|                         continue |                         continue | ||||||
|                     } |                     } | ||||||
|                     loadedTiles.add(key) |                     loadedTiles.add(key) | ||||||
|                     this.GetIdb(key).then((features: { feature: any, freshness: Date }[]) => { |                     this.GetIdb(key).then((features: { feature: any; freshness: Date }[]) => { | ||||||
|                         if(features === undefined){ |                         if (features === undefined) { | ||||||
|                             return; |                             return | ||||||
|                         } |                         } | ||||||
|                         console.debug("Loaded tile " + self._layer.id + "_" + key + " from disk") |                         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) |                         registerTile(src) | ||||||
|                     }) |                     }) | ||||||
|                 } |                 } | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
|             return true; // Remove the callback
 |             return true // Remove the callback
 | ||||||
| 
 |  | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public addTile(tile: FeatureSource & Tiled) { |     public addTile(tile: FeatureSource & Tiled) { | ||||||
|         const self = this |         const self = this | ||||||
|         tile.features.addCallbackAndRunD(features => { |         tile.features.addCallbackAndRunD((features) => { | ||||||
|             const now = new Date() |             const now = new Date() | ||||||
| 
 | 
 | ||||||
|             if (features.length > 0) { |             if (features.length > 0) { | ||||||
|  | @ -109,11 +116,10 @@ export default class SaveTileToLocalStorageActor { | ||||||
| 
 | 
 | ||||||
|     public poison(lon: number, lat: number) { |     public poison(lon: number, lat: number) { | ||||||
|         for (let z = 0; z < 25; z++) { |         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) |             const tileId = Tiles.tile_index(z, x, y) | ||||||
|             this.visitedTiles.data.delete(tileId) |             this.visitedTiles.data.delete(tileId) | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public MarkVisited(tileId: number, freshness: Date) { |     public MarkVisited(tileId: number, freshness: Date) { | ||||||
|  | @ -125,11 +131,18 @@ export default class SaveTileToLocalStorageActor { | ||||||
|         try { |         try { | ||||||
|             IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data) |             IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data) | ||||||
|         } catch (e) { |         } 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 | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private GetIdb(tileIndex) { |     private GetIdb(tileIndex) { | ||||||
|         return IdbLocalStorage.GetDirectly(this._layer.id + "_" + tileIndex) |         return IdbLocalStorage.GetDirectly(this._layer.id + "_" + tileIndex) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,34 +1,33 @@ | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import FilteringFeatureSource from "./Sources/FilteringFeatureSource"; | import FilteringFeatureSource from "./Sources/FilteringFeatureSource" | ||||||
| import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter"; | import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter" | ||||||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "./FeatureSource"; | import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "./FeatureSource" | ||||||
| import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource"; | import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource" | ||||||
| import {Store, UIEventSource} from "../UIEventSource"; | import { Store, UIEventSource } from "../UIEventSource" | ||||||
| import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy"; | import { TileHierarchyTools } from "./TiledFeatureSource/TileHierarchy" | ||||||
| import RememberingSource from "./Sources/RememberingSource"; | import RememberingSource from "./Sources/RememberingSource" | ||||||
| import OverpassFeatureSource from "../Actors/OverpassFeatureSource"; | import OverpassFeatureSource from "../Actors/OverpassFeatureSource" | ||||||
| import GeoJsonSource from "./Sources/GeoJsonSource"; | import GeoJsonSource from "./Sources/GeoJsonSource" | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc" | ||||||
| import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor"; | import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor" | ||||||
| import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor"; | import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor" | ||||||
| import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource"; | import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource" | ||||||
| import {TileHierarchyMerger} from "./TiledFeatureSource/TileHierarchyMerger"; | import { TileHierarchyMerger } from "./TiledFeatureSource/TileHierarchyMerger" | ||||||
| import RelationsTracker from "../Osm/RelationsTracker"; | import RelationsTracker from "../Osm/RelationsTracker" | ||||||
| import {NewGeometryFromChangesFeatureSource} from "./Sources/NewGeometryFromChangesFeatureSource"; | import { NewGeometryFromChangesFeatureSource } from "./Sources/NewGeometryFromChangesFeatureSource" | ||||||
| import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator"; | import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator" | ||||||
| import {BBox} from "../BBox"; | import { BBox } from "../BBox" | ||||||
| import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource"; | import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource" | ||||||
| import {Tiles} from "../../Models/TileRange"; | import { Tiles } from "../../Models/TileRange" | ||||||
| import TileFreshnessCalculator from "./TileFreshnessCalculator"; | import TileFreshnessCalculator from "./TileFreshnessCalculator" | ||||||
| import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource"; | import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource" | ||||||
| import MapState from "../State/MapState"; | import MapState from "../State/MapState" | ||||||
| import {ElementStorage} from "../ElementStorage"; | import { ElementStorage } from "../ElementStorage" | ||||||
| import {OsmFeature} from "../../Models/OsmFeature"; | import { OsmFeature } from "../../Models/OsmFeature" | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
| import {FilterState} from "../../Models/FilteredLayer"; | import { FilterState } from "../../Models/FilteredLayer" | ||||||
| import {GeoOperations} from "../GeoOperations"; | import { GeoOperations } from "../GeoOperations" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The features pipeline ties together a myriad of various datasources: |  * The features pipeline ties together a myriad of various datasources: | ||||||
|  | @ -42,12 +41,12 @@ import {Utils} from "../../Utils"; | ||||||
|  * |  * | ||||||
|  */ |  */ | ||||||
| export default class FeaturePipeline { | export default class FeaturePipeline { | ||||||
| 
 |     public readonly sufficientlyZoomed: Store<boolean> | ||||||
|     public readonly sufficientlyZoomed: Store<boolean>; |     public readonly runningQuery: Store<boolean> | ||||||
|     public readonly runningQuery: Store<boolean>; |     public readonly timeout: UIEventSource<number> | ||||||
|     public readonly timeout: UIEventSource<number>; |  | ||||||
|     public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false) |     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 |     public readonly relationTracker: RelationsTracker | ||||||
|     /** |     /** | ||||||
|      * Keeps track of all raw OSM-nodes. |      * Keeps track of all raw OSM-nodes. | ||||||
|  | @ -55,19 +54,19 @@ export default class FeaturePipeline { | ||||||
|      */ |      */ | ||||||
|     public readonly fullNodeDatabase?: FullNodeDatabaseSource |     public readonly fullNodeDatabase?: FullNodeDatabaseSource | ||||||
|     private readonly overpassUpdater: OverpassFeatureSource |     private readonly overpassUpdater: OverpassFeatureSource | ||||||
|     private state: MapState; |     private state: MapState | ||||||
|     private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>; |     private readonly perLayerHierarchy: Map<string, TileHierarchyMerger> | ||||||
|     /** |     /** | ||||||
|      * Keeps track of the age of the loaded data. |      * Keeps track of the age of the loaded data. | ||||||
|      * Has one freshness-Calculator for every layer |      * Has one freshness-Calculator for every layer | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private readonly freshnesses = new Map<string, TileFreshnessCalculator>(); |     private readonly freshnesses = new Map<string, TileFreshnessCalculator>() | ||||||
|     private readonly oldestAllowedDate: Date; |     private readonly oldestAllowedDate: Date | ||||||
|     private readonly osmSourceZoomLevel |     private readonly osmSourceZoomLevel | ||||||
|     private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>() |     private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>() | ||||||
|      | 
 | ||||||
|     private readonly newGeometryHandler : NewGeometryFromChangesFeatureSource; |     private readonly newGeometryHandler: NewGeometryFromChangesFeatureSource | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, |         handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, | ||||||
|  | @ -77,33 +76,40 @@ export default class FeaturePipeline { | ||||||
|             handleRawFeatureSource: (source: FeatureSourceForLayer) => void |             handleRawFeatureSource: (source: FeatureSourceForLayer) => void | ||||||
|         } |         } | ||||||
|     ) { |     ) { | ||||||
|         this.state = state; |         this.state = state | ||||||
| 
 | 
 | ||||||
|         const self = this |         const self = this | ||||||
|         const expiryInSeconds = Math.min(...state.layoutToUse?.layers?.map(l => l.maxAgeOfCache) ?? []) |         const expiryInSeconds = Math.min( | ||||||
|         this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds); |             ...(state.layoutToUse?.layers?.map((l) => l.maxAgeOfCache) ?? []) | ||||||
|         this.osmSourceZoomLevel = state.osmApiTileSize.data; |         ) | ||||||
|         const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12)) |         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() |         this.relationTracker = new RelationsTracker() | ||||||
| 
 | 
 | ||||||
|         state.changes.allChanges.addCallbackAndRun(allChanges => { |         state.changes.allChanges.addCallbackAndRun((allChanges) => { | ||||||
|             allChanges.filter(ch => ch.id < 0 && ch.changes !== undefined) |             allChanges | ||||||
|                 .map(ch => ch.changes) |                 .filter((ch) => ch.id < 0 && ch.changes !== undefined) | ||||||
|                 .filter(coor => coor["lat"] !== undefined && coor["lon"] !== undefined) |                 .map((ch) => ch.changes) | ||||||
|                 .forEach(coor => { |                 .filter((coor) => coor["lat"] !== undefined && coor["lon"] !== undefined) | ||||||
|                     state.layoutToUse.layers.forEach(l => self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"])) |                 .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) { | ||||||
|                 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) |         const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed) | ||||||
| 
 | 
 | ||||||
|  | @ -111,9 +117,11 @@ export default class FeaturePipeline { | ||||||
|         this.perLayerHierarchy = perLayerHierarchy |         this.perLayerHierarchy = perLayerHierarchy | ||||||
| 
 | 
 | ||||||
|         // Given a tile, wraps it and passes it on to render (handled by 'handleFeatureSource'
 |         // 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
 |             // 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) |             const srcFiltered = new FilteringFeatureSource(state, src.tileIndex, withChanges) | ||||||
| 
 | 
 | ||||||
|             handleFeatureSource(srcFiltered) |             handleFeatureSource(srcFiltered) | ||||||
|  | @ -127,31 +135,29 @@ export default class FeaturePipeline { | ||||||
|         function handlePriviligedFeatureSource(src: FeatureSourceForLayer & Tiled) { |         function handlePriviligedFeatureSource(src: FeatureSourceForLayer & Tiled) { | ||||||
|             // Passthrough to passed function, except that it registers as well
 |             // Passthrough to passed function, except that it registers as well
 | ||||||
|             handleFeatureSource(src) |             handleFeatureSource(src) | ||||||
|             src.features.addCallbackAndRunD(fs => { |             src.features.addCallbackAndRunD((fs) => { | ||||||
|                 fs.forEach(ff => state.allElements.addOrGetElement(ff.feature)) |                 fs.forEach((ff) => state.allElements.addOrGetElement(ff.feature)) | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         for (const filteredLayer of state.filteredLayers.data) { |         for (const filteredLayer of state.filteredLayers.data) { | ||||||
|             const id = filteredLayer.layerDef.id |             const id = filteredLayer.layerDef.id | ||||||
|             const source = filteredLayer.layerDef.source |             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) |             perLayerHierarchy.set(id, hierarchy) | ||||||
| 
 | 
 | ||||||
|             this.freshnesses.set(id, new TileFreshnessCalculator()) |             this.freshnesses.set(id, new TileFreshnessCalculator()) | ||||||
| 
 | 
 | ||||||
|             if (id === "type_node") { |             if (id === "type_node") { | ||||||
| 
 |                 this.fullNodeDatabase = new FullNodeDatabaseSource(filteredLayer, (tile) => { | ||||||
|                 this.fullNodeDatabase = new FullNodeDatabaseSource( |                     new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||||
|                     filteredLayer, |                     perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) | ||||||
|                     tile => { |                     tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) | ||||||
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |                 }) | ||||||
|                         perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) |                 continue | ||||||
|                         tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) |  | ||||||
|                     }); |  | ||||||
|                 continue; |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (id === "gps_location") { |             if (id === "gps_location") { | ||||||
|  | @ -187,13 +193,15 @@ export default class FeaturePipeline { | ||||||
|                 // We load the cached values and register them
 |                 // We load the cached values and register them
 | ||||||
|                 // Getting data from upstream happens a bit lower
 |                 // Getting data from upstream happens a bit lower
 | ||||||
|                 localTileSaver.LoadTilesFromDisk( |                 localTileSaver.LoadTilesFromDisk( | ||||||
|                     state.currentBounds, state.locationControl, |                     state.currentBounds, | ||||||
|                     (tileIndex, freshness) => self.freshnesses.get(id).addTileLoad(tileIndex, freshness), |                     state.locationControl, | ||||||
|  |                     (tileIndex, freshness) => | ||||||
|  |                         self.freshnesses.get(id).addTileLoad(tileIndex, freshness), | ||||||
|                     (tile) => { |                     (tile) => { | ||||||
|                         console.debug("Loaded tile ", id, tile.tileIndex, "from local cache") |                         console.debug("Loaded tile ", id, tile.tileIndex, "from local cache") | ||||||
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||||
|                         hierarchy.registerTile(tile); |                         hierarchy.registerTile(tile) | ||||||
|                         tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) |                         tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) | ||||||
|                     } |                     } | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|  | @ -213,47 +221,48 @@ export default class FeaturePipeline { | ||||||
|                         registerTile: (tile) => { |                         registerTile: (tile) => { | ||||||
|                             new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |                             new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||||
|                             perLayerHierarchy.get(id).registerTile(tile) |                             perLayerHierarchy.get(id).registerTile(tile) | ||||||
|                             tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) |                             tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) | ||||||
|                         } |                         }, | ||||||
|                     }) |                     }) | ||||||
|                 } else { |                 } else { | ||||||
|                     new RegisteringAllFromFeatureSourceActor(src, state.allElements) |                     new RegisteringAllFromFeatureSourceActor(src, state.allElements) | ||||||
|                     perLayerHierarchy.get(id).registerTile(src) |                     perLayerHierarchy.get(id).registerTile(src) | ||||||
|                     src.features.addCallbackAndRunD(_ => self.onNewDataLoaded(src)) |                     src.features.addCallbackAndRunD((_) => self.onNewDataLoaded(src)) | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|                 new DynamicGeoJsonTileSource( |                 new DynamicGeoJsonTileSource( | ||||||
|                     filteredLayer, |                     filteredLayer, | ||||||
|                     tile => { |                     (tile) => { | ||||||
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||||
|                         perLayerHierarchy.get(id).registerTile(tile) |                         perLayerHierarchy.get(id).registerTile(tile) | ||||||
|                         tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) |                         tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) | ||||||
|                     }, |                     }, | ||||||
|                     state |                     state | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         const osmFeatureSource = new OsmFeatureSource({ |         const osmFeatureSource = new OsmFeatureSource({ | ||||||
|             isActive: useOsmApi, |             isActive: useOsmApi, | ||||||
|             neededTiles: neededTilesFromOsm, |             neededTiles: neededTilesFromOsm, | ||||||
|             handleTile: tile => { |             handleTile: (tile) => { | ||||||
|                 new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |                 new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||||
|                 if (tile.layer.layerDef.maxAgeOfCache > 0) { |                 if (tile.layer.layerDef.maxAgeOfCache > 0) { | ||||||
|                     const saver = self.localStorageSavers.get(tile.layer.layerDef.id) |                     const saver = self.localStorageSavers.get(tile.layer.layerDef.id) | ||||||
|                     if (saver === undefined) { |                     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) |                     saver?.addTile(tile) | ||||||
|                 } |                 } | ||||||
|                 perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) |                 perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) | ||||||
|                 tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) |                 tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) | ||||||
| 
 |  | ||||||
|             }, |             }, | ||||||
|             state: state, |             state: state, | ||||||
|             markTileVisited: (tileId) => |             markTileVisited: (tileId) => | ||||||
|                 state.filteredLayers.data.forEach(flayer => { |                 state.filteredLayers.data.forEach((flayer) => { | ||||||
|                     const layer = flayer.layerDef |                     const layer = flayer.layerDef | ||||||
|                     if (layer.maxAgeOfCache > 0) { |                     if (layer.maxAgeOfCache > 0) { | ||||||
|                         const saver = self.localStorageSavers.get(layer.id) |                         const saver = self.localStorageSavers.get(layer.id) | ||||||
|  | @ -264,110 +273,128 @@ export default class FeaturePipeline { | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                     self.freshnesses.get(layer.id).addTileLoad(tileId, new Date()) |                     self.freshnesses.get(layer.id).addTileLoad(tileId, new Date()) | ||||||
|                 }) |                 }), | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         if (this.fullNodeDatabase !== undefined) { |         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) |         const updater = this.initOverpassUpdater(state, useOsmApi) | ||||||
|         this.overpassUpdater = updater; |         this.overpassUpdater = updater | ||||||
|         this.timeout = updater.timeout |         this.timeout = updater.timeout | ||||||
| 
 | 
 | ||||||
|         // Actually load data from the overpass source
 |         // Actually load data from the overpass source
 | ||||||
|         new PerLayerFeatureSourceSplitter(state.filteredLayers, |         new PerLayerFeatureSourceSplitter( | ||||||
|             (source) => TiledFeatureSource.createHierarchy(source, { |             state.filteredLayers, | ||||||
|                 layer: source.layer, |             (source) => | ||||||
|                 minZoomLevel: source.layer.layerDef.minzoom, |                 TiledFeatureSource.createHierarchy(source, { | ||||||
|                 noDuplicates: true, |                     layer: source.layer, | ||||||
|                 maxFeatureCount: state.layoutToUse.clustering.minNeededElements, |                     minZoomLevel: source.layer.layerDef.minzoom, | ||||||
|                 maxZoomLevel: state.layoutToUse.clustering.maxZoom, |                     noDuplicates: true, | ||||||
|                 registerTile: (tile) => { |                     maxFeatureCount: state.layoutToUse.clustering.minNeededElements, | ||||||
|                     // We save the tile data for the given layer to local storage - data sourced from overpass
 |                     maxZoomLevel: state.layoutToUse.clustering.maxZoom, | ||||||
|                     self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile) |                     registerTile: (tile) => { | ||||||
|                     perLayerHierarchy.get(source.layer.layerDef.id).registerTile(new RememberingSource(tile)) |                         // We save the tile data for the given layer to local storage - data sourced from overpass
 | ||||||
|                     tile.features.addCallbackAndRunD(f => { |                         self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile) | ||||||
|                         if (f.length === 0) { |                         perLayerHierarchy | ||||||
|                             return |                             .get(source.layer.layerDef.id) | ||||||
|                         } |                             .registerTile(new RememberingSource(tile)) | ||||||
|                         self.onNewDataLoaded(tile) |                         tile.features.addCallbackAndRunD((f) => { | ||||||
|                     }) |                             if (f.length === 0) { | ||||||
| 
 |                                 return | ||||||
|                 } |                             } | ||||||
|             }), |                             self.onNewDataLoaded(tile) | ||||||
|  |                         }) | ||||||
|  |                     }, | ||||||
|  |                 }), | ||||||
|             updater, |             updater, | ||||||
|             { |             { | ||||||
|                 handleLeftovers: (leftOvers) => { |                 handleLeftovers: (leftOvers) => { | ||||||
|                     console.warn("Overpass returned a few non-matched features:", leftOvers) |                     console.warn("Overpass returned a few non-matched features:", leftOvers) | ||||||
|                 } |                 }, | ||||||
|             }) |             } | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
| 
 |         // Also load points/lines that are newly added.
 | ||||||
|         // Also load points/lines that are newly added. 
 |         const newGeometry = new NewGeometryFromChangesFeatureSource( | ||||||
|         const newGeometry = new NewGeometryFromChangesFeatureSource(state.changes, state.allElements, state.osmConnection._oauth_config.url) |             state.changes, | ||||||
|         this.newGeometryHandler = newGeometry; |             state.allElements, | ||||||
|         newGeometry.features.addCallbackAndRun(geometries => { |             state.osmConnection._oauth_config.url | ||||||
|  |         ) | ||||||
|  |         this.newGeometryHandler = newGeometry | ||||||
|  |         newGeometry.features.addCallbackAndRun((geometries) => { | ||||||
|             console.debug("New geometries are:", geometries) |             console.debug("New geometries are:", geometries) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         new RegisteringAllFromFeatureSourceActor(newGeometry, state.allElements) |         new RegisteringAllFromFeatureSourceActor(newGeometry, state.allElements) | ||||||
|         // A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next
 |         // A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next
 | ||||||
|         new PerLayerFeatureSourceSplitter(state.filteredLayers, |         new PerLayerFeatureSourceSplitter( | ||||||
|  |             state.filteredLayers, | ||||||
|             (perLayer) => { |             (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
 |                 // 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) |                 perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer) | ||||||
|                 // AT last, we always apply the metatags whenever possible
 |                 // AT last, we always apply the metatags whenever possible
 | ||||||
|                 perLayer.features.addCallbackAndRunD(_ => { |                 perLayer.features.addCallbackAndRunD((_) => { | ||||||
|                     self.onNewDataLoaded(perLayer); |                     self.onNewDataLoaded(perLayer) | ||||||
|                 }) |                 }) | ||||||
| 
 |  | ||||||
|             }, |             }, | ||||||
|             newGeometry, |             newGeometry, | ||||||
|             { |             { | ||||||
|                 handleLeftovers: (leftOvers) => { |                 handleLeftovers: (leftOvers) => { | ||||||
|                     console.warn("Got some leftovers from the filteredLayers: ", leftOvers) |                     console.warn("Got some leftovers from the filteredLayers: ", leftOvers) | ||||||
|                 } |                 }, | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         this.runningQuery = updater.runningQuery.map( |         this.runningQuery = updater.runningQuery.map( | ||||||
|             overpass => { |             (overpass) => { | ||||||
|                 console.log("FeaturePipeline: runningQuery state changed: Overpass", overpass ? "is querying," : "is idle,", |                 console.log( | ||||||
|                     "osmFeatureSource is", osmFeatureSource.isRunning ? "is running and needs " + neededTilesFromOsm.data?.length + " tiles (already got " + osmFeatureSource.downloadedTiles.size + " tiles )" : "is idle") |                     "FeaturePipeline: runningQuery state changed: Overpass", | ||||||
|                 return overpass || osmFeatureSource.isRunning.data; |                     overpass ? "is querying," : "is idle,", | ||||||
|             }, [osmFeatureSource.isRunning] |                     "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[][] { |     public GetAllFeaturesWithin(bbox: BBox): OsmFeature[][] { | ||||||
|         const self = this |         const self = this | ||||||
|         const tiles: OsmFeature[][] = [] |         const tiles: OsmFeature[][] = [] | ||||||
|         Array.from(this.perLayerHierarchy.keys()) |         Array.from(this.perLayerHierarchy.keys()).forEach((key) => { | ||||||
|             .forEach(key => { |             const fetched: OsmFeature[][] = self.GetFeaturesWithin(key, bbox) | ||||||
|                 const fetched : OsmFeature[][] = self.GetFeaturesWithin(key, bbox) |             tiles.push(...fetched) | ||||||
|                 tiles.push(...fetched); |         }) | ||||||
|             }) |         return tiles | ||||||
|         return tiles; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GetAllFeaturesAndMetaWithin(bbox: BBox, layerIdWhitelist?: Set<string>):  |     public GetAllFeaturesAndMetaWithin( | ||||||
|         {features: OsmFeature[], layer: string}[] { |         bbox: BBox, | ||||||
|  |         layerIdWhitelist?: Set<string> | ||||||
|  |     ): { features: OsmFeature[]; layer: string }[] { | ||||||
|         const self = this |         const self = this | ||||||
|         const tiles :{features: any[], layer: string}[]= [] |         const tiles: { features: any[]; layer: string }[] = [] | ||||||
|         Array.from(this.perLayerHierarchy.keys()) |         Array.from(this.perLayerHierarchy.keys()).forEach((key) => { | ||||||
|             .forEach(key => { |             if (layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)) { | ||||||
|                 if(layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)){ |                 return | ||||||
|                     return; |             } | ||||||
|                 } |             return tiles.push({ | ||||||
|                 return tiles.push({ |                 layer: key, | ||||||
|                     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) |         const requestedHierarchy = this.perLayerHierarchy.get(layerId) | ||||||
|         if (requestedHierarchy === undefined) { |         if (requestedHierarchy === undefined) { | ||||||
|             console.warn("Layer ", layerId, "is not defined. Try one of ", Array.from(this.perLayerHierarchy.keys())) |             console.warn( | ||||||
|             return undefined; |                 "Layer ", | ||||||
|  |                 layerId, | ||||||
|  |                 "is not defined. Try one of ", | ||||||
|  |                 Array.from(this.perLayerHierarchy.keys()) | ||||||
|  |             ) | ||||||
|  |             return undefined | ||||||
|         } |         } | ||||||
|         return TileHierarchyTools.getTiles(requestedHierarchy, bbox) |         return TileHierarchyTools.getTiles(requestedHierarchy, bbox) | ||||||
|             .filter(featureSource => featureSource.features?.data !== undefined) |             .filter((featureSource) => featureSource.features?.data !== undefined) | ||||||
|             .map(featureSource => featureSource.features.data.map(fs => fs.feature)) |             .map((featureSource) => featureSource.features.data.map((fs) => fs.feature)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GetTilesPerLayerWithin(bbox: BBox, handleTile: (tile: FeatureSourceForLayer & Tiled) => void) { |     public GetTilesPerLayerWithin( | ||||||
|         Array.from(this.perLayerHierarchy.values()).forEach(hierarchy => { |         bbox: BBox, | ||||||
|  |         handleTile: (tile: FeatureSourceForLayer & Tiled) => void | ||||||
|  |     ) { | ||||||
|  |         Array.from(this.perLayerHierarchy.values()).forEach((hierarchy) => { | ||||||
|             TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile) |             TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile) | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  | @ -399,16 +434,16 @@ export default class FeaturePipeline { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private freshnessForVisibleLayers(z: number, x: number, y: number): Date { |     private freshnessForVisibleLayers(z: number, x: number, y: number): Date { | ||||||
|         let oldestDate = undefined; |         let oldestDate = undefined | ||||||
|         for (const flayer of this.state.filteredLayers.data) { |         for (const flayer of this.state.filteredLayers.data) { | ||||||
|             if (!flayer.isDisplayed.data && !flayer.layerDef.forceLoad) { |             if (!flayer.isDisplayed.data && !flayer.layerDef.forceLoad) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             if (this.state.locationControl.data.zoom < flayer.layerDef.minzoom) { |             if (this.state.locationControl.data.zoom < flayer.layerDef.minzoom) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
|             if (flayer.layerDef.maxAgeOfCache === 0) { |             if (flayer.layerDef.maxAgeOfCache === 0) { | ||||||
|                 return undefined; |                 return undefined | ||||||
|             } |             } | ||||||
|             const freshnessCalc = this.freshnesses.get(flayer.layerDef.id) |             const freshnessCalc = this.freshnesses.get(flayer.layerDef.id) | ||||||
|             if (freshnessCalc === undefined) { |             if (freshnessCalc === undefined) { | ||||||
|  | @ -428,117 +463,136 @@ export default class FeaturePipeline { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /* |     /* | ||||||
|     * Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM |      * Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM | ||||||
|     * */ |      * */ | ||||||
|     private getNeededTilesFromOsm(isSufficientlyZoomed: Store<boolean>): Store<number[]> { |     private getNeededTilesFromOsm(isSufficientlyZoomed: Store<boolean>): Store<number[]> { | ||||||
|         const self = this |         const self = this | ||||||
|         return this.state.currentBounds.map(bbox => { |         return this.state.currentBounds.map( | ||||||
|             if (bbox === undefined) { |             (bbox) => { | ||||||
|                 return [] |                 if (bbox === undefined) { | ||||||
|             } |                     return [] | ||||||
|             if (!isSufficientlyZoomed.data) { |  | ||||||
|                 return []; |  | ||||||
|             } |  | ||||||
|             const osmSourceZoomLevel = self.osmSourceZoomLevel |  | ||||||
|             const range = bbox.containingTileRange(osmSourceZoomLevel) |  | ||||||
|             const tileIndexes = [] |  | ||||||
|             if (range.total >= 100) { |  | ||||||
|                 // Too much tiles!
 |  | ||||||
|                 return undefined |  | ||||||
|             } |  | ||||||
|             Tiles.MapRange(range, (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") |  | ||||||
|                     // The cached tiles contain decently fresh data
 |  | ||||||
|                     return undefined; |  | ||||||
|                 } |                 } | ||||||
|                 tileIndexes.push(i) |                 if (!isSufficientlyZoomed.data) { | ||||||
|             }) |                     return [] | ||||||
|             return tileIndexes |                 } | ||||||
|         }, [isSufficientlyZoomed]) |                 const osmSourceZoomLevel = self.osmSourceZoomLevel | ||||||
|  |                 const range = bbox.containingTileRange(osmSourceZoomLevel) | ||||||
|  |                 const tileIndexes = [] | ||||||
|  |                 if (range.total >= 100) { | ||||||
|  |                     // Too much tiles!
 | ||||||
|  |                     return undefined | ||||||
|  |                 } | ||||||
|  |                 Tiles.MapRange(range, (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" | ||||||
|  |                         ) | ||||||
|  |                         // The cached tiles contain decently fresh data
 | ||||||
|  |                         return undefined | ||||||
|  |                     } | ||||||
|  |                     tileIndexes.push(i) | ||||||
|  |                 }) | ||||||
|  |                 return tileIndexes | ||||||
|  |             }, | ||||||
|  |             [isSufficientlyZoomed] | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private initOverpassUpdater(state: { |     private initOverpassUpdater( | ||||||
|         allElements: ElementStorage; |         state: { | ||||||
|         layoutToUse: LayoutConfig, |             allElements: ElementStorage | ||||||
|         currentBounds: Store<BBox>, |             layoutToUse: LayoutConfig | ||||||
|         locationControl: Store<Loc>, |             currentBounds: Store<BBox> | ||||||
|         readonly overpassUrl: Store<string[]>; |             locationControl: Store<Loc> | ||||||
|         readonly overpassTimeout: Store<number>; |             readonly overpassUrl: Store<string[]> | ||||||
|         readonly overpassMaxZoom: Store<number>, |             readonly overpassTimeout: Store<number> | ||||||
|     }, useOsmApi: Store<boolean>): OverpassFeatureSource { |             readonly overpassMaxZoom: Store<number> | ||||||
|         const minzoom = Math.min(...state.layoutToUse.layers.map(layer => layer.minzoom)) |         }, | ||||||
|         const overpassIsActive = state.currentBounds.map(bbox => { |         useOsmApi: Store<boolean> | ||||||
|             if (bbox === undefined) { |     ): OverpassFeatureSource { | ||||||
|                 console.debug("Disabling overpass source: no bbox") |         const minzoom = Math.min(...state.layoutToUse.layers.map((layer) => layer.minzoom)) | ||||||
|                 return false |         const overpassIsActive = state.currentBounds.map( | ||||||
|             } |             (bbox) => { | ||||||
|             let zoom = state.locationControl.data.zoom |                 if (bbox === undefined) { | ||||||
|             if (zoom < minzoom) { |                     console.debug("Disabling overpass source: no bbox") | ||||||
|                 // We are zoomed out over the zoomlevel of any layer
 |                     return false | ||||||
|                 console.debug("Disabling overpass source: zoom < minzoom") |                 } | ||||||
|                 return false; |                 let zoom = state.locationControl.data.zoom | ||||||
|             } |                 if (zoom < minzoom) { | ||||||
| 
 |                     // We are zoomed out over the zoomlevel of any layer
 | ||||||
|             const range = bbox.containingTileRange(zoom) |                     console.debug("Disabling overpass source: zoom < minzoom") | ||||||
|             if (range.total >= 5000) { |                     return false | ||||||
|                 // 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 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]), |  | ||||||
|                 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 => { |  | ||||||
|                             self.freshnesses.get(layer.id).addTileLoad(tileIndex, date) |  | ||||||
|                             self.localStorageSavers.get(layer.id)?.MarkVisited(tileIndex, date) |  | ||||||
|                         }) |  | ||||||
|                     }) |  | ||||||
| 
 |  | ||||||
|                 } |                 } | ||||||
|             }); |  | ||||||
| 
 | 
 | ||||||
|  |                 const range = bbox.containingTileRange(zoom) | ||||||
|  |                 if (range.total >= 5000) { | ||||||
|  |                     // 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 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]), | ||||||
|  |             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) => { | ||||||
|  |                         self.freshnesses.get(layer.id).addTileLoad(tileIndex, date) | ||||||
|  |                         self.localStorageSavers.get(layer.id)?.MarkVisited(tileIndex, date) | ||||||
|  |                     }) | ||||||
|  |                 }) | ||||||
|  |             }, | ||||||
|  |         }) | ||||||
| 
 | 
 | ||||||
|         // Register everything in the state' 'AllElements'
 |         // Register everything in the state' 'AllElements'
 | ||||||
|         new RegisteringAllFromFeatureSourceActor(updater, state.allElements) |         new RegisteringAllFromFeatureSourceActor(updater, state.allElements) | ||||||
|         return updater; |         return updater | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Builds upon 'GetAllFeaturesAndMetaWithin', but does stricter BBOX-checking and applies the filters |      * 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) { |         if (bbox === undefined) { | ||||||
|             console.warn("No bbox") |             console.warn("No bbox") | ||||||
|             return [] |             return [] | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const layers = Utils.toIdRecord(this.state.layoutToUse.layers) |         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>() |         let seenElements = new Set<string>() | ||||||
|         for (const elementsWithMetaElement of elementsWithMeta) { |         for (const elementsWithMetaElement of elementsWithMeta) { | ||||||
|             const layer = layers[elementsWithMetaElement.layer] |             const layer = layers[elementsWithMetaElement.layer] | ||||||
|             if(layer.title === undefined){ |             if (layer.title === undefined) { | ||||||
|                 continue |                 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++) { |             for (let i = 0; i < elementsWithMetaElement.features.length; i++) { | ||||||
|                 const element = elementsWithMetaElement.features[i]; |                 const element = elementsWithMetaElement.features[i] | ||||||
|                 if (!filtered.isDisplayed.data) { |                 if (!filtered.isDisplayed.data) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|  | @ -552,35 +606,38 @@ export default class FeaturePipeline { | ||||||
|                 if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) { |                 if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|                 const activeFilters: FilterState[] = Array.from(filtered.appliedFilters.data.values()); |                 const activeFilters: FilterState[] = Array.from( | ||||||
|                 if (!activeFilters.every(filter => filter?.currentFilter === undefined || filter?.currentFilter?.matchesProperties(element.properties))) { |                     filtered.appliedFilters.data.values() | ||||||
|  |                 ) | ||||||
|  |                 if ( | ||||||
|  |                     !activeFilters.every( | ||||||
|  |                         (filter) => | ||||||
|  |                             filter?.currentFilter === undefined || | ||||||
|  |                             filter?.currentFilter?.matchesProperties(element.properties) | ||||||
|  |                     ) | ||||||
|  |                 ) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|                 const center = GeoOperations.centerpointCoordinates(element); |                 const center = GeoOperations.centerpointCoordinates(element) | ||||||
|                 elements.push({ |                 elements.push({ | ||||||
|                     element, |                     element, | ||||||
|                     center, |                     center, | ||||||
|                     layer: layers[elementsWithMetaElement.layer], |                     layer: layers[elementsWithMetaElement.layer], | ||||||
|                 }) |                 }) | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |         return elements | ||||||
|       |  | ||||||
| 
 |  | ||||||
|         return elements; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|     * Inject a new point |      * Inject a new point | ||||||
|      */ |      */ | ||||||
|     InjectNewPoint(geojson) { |     InjectNewPoint(geojson) { | ||||||
|         this.newGeometryHandler.features.data.push({ |         this.newGeometryHandler.features.data.push({ | ||||||
|             feature: geojson, |             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 { Store, UIEventSource } from "../UIEventSource" | ||||||
| import FilteredLayer from "../../Models/FilteredLayer"; | import FilteredLayer from "../../Models/FilteredLayer" | ||||||
| import {BBox} from "../BBox"; | import { BBox } from "../BBox" | ||||||
| import {Feature, Geometry} from "@turf/turf"; | import { Feature, Geometry } from "@turf/turf" | ||||||
| import {OsmFeature} from "../../Models/OsmFeature"; | import { OsmFeature } from "../../Models/OsmFeature" | ||||||
| 
 | 
 | ||||||
| export default interface FeatureSource { | export default interface FeatureSource { | ||||||
|     features: Store<{ feature: OsmFeature, freshness: Date }[]>; |     features: Store<{ feature: OsmFeature; freshness: Date }[]> | ||||||
|     /** |     /** | ||||||
|      * Mainly used for debuging |      * Mainly used for debuging | ||||||
|      */ |      */ | ||||||
|     name: string; |     name: string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface Tiled { | export interface Tiled { | ||||||
|     tileIndex: number, |     tileIndex: number | ||||||
|     bbox: BBox |     bbox: BBox | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,7 @@ | ||||||
| import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource"; | import FeatureSource, { FeatureSourceForLayer, Tiled } from "./FeatureSource" | ||||||
| import {Store} from "../UIEventSource"; | import { Store } from "../UIEventSource" | ||||||
| import FilteredLayer from "../../Models/FilteredLayer"; | import FilteredLayer from "../../Models/FilteredLayer" | ||||||
| import SimpleFeatureSource from "./Sources/SimpleFeatureSource"; | import SimpleFeatureSource from "./Sources/SimpleFeatureSource" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) |  * 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 |  * In any case, this featureSource marks the objects with _matching_layer_id | ||||||
|  */ |  */ | ||||||
| export default class PerLayerFeatureSourceSplitter { | export default class PerLayerFeatureSourceSplitter { | ||||||
| 
 |     constructor( | ||||||
|     constructor(layers: Store<FilteredLayer[]>, |         layers: Store<FilteredLayer[]>, | ||||||
|                 handleLayerData: (source: FeatureSourceForLayer & Tiled) => void, |         handleLayerData: (source: FeatureSourceForLayer & Tiled) => void, | ||||||
|                 upstream: FeatureSource, |         upstream: FeatureSource, | ||||||
|                 options?: { |         options?: { | ||||||
|                     tileIndex?: number, |             tileIndex?: number | ||||||
|                     handleLeftovers?: (featuresWithoutLayer: any[]) => void |             handleLeftovers?: (featuresWithoutLayer: any[]) => void | ||||||
|                 }) { |         } | ||||||
| 
 |     ) { | ||||||
|         const knownLayers = new Map<string, SimpleFeatureSource>() |         const knownLayers = new Map<string, SimpleFeatureSource>() | ||||||
| 
 | 
 | ||||||
|         function update() { |         function update() { | ||||||
|             const features = upstream.features?.data; |             const features = upstream.features?.data | ||||||
|             if (features === undefined) { |             if (features === undefined) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             if (layers.data === undefined || layers.data.length === 0) { |             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.
 |             // 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
 |             // 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 = [] |             const noLayerFound = [] | ||||||
| 
 | 
 | ||||||
|             for (const layer of layers.data) { |             for (const layer of layers.data) { | ||||||
|  | @ -41,19 +40,19 @@ export default class PerLayerFeatureSourceSplitter { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             for (const f of features) { |             for (const f of features) { | ||||||
|                 let foundALayer = false; |                 let foundALayer = false | ||||||
|                 for (const layer of layers.data) { |                 for (const layer of layers.data) { | ||||||
|                     if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) { |                     if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) { | ||||||
|                         // We have found our matching layer!
 |                         // We have found our matching layer!
 | ||||||
|                         featuresPerLayer.get(layer.layerDef.id).push(f) |                         featuresPerLayer.get(layer.layerDef.id).push(f) | ||||||
|                         foundALayer = true; |                         foundALayer = true | ||||||
|                         if (!layer.layerDef.passAllFeatures) { |                         if (!layer.layerDef.passAllFeatures) { | ||||||
|                             // If not 'passAllFeatures', we are done for this feature
 |                             // If not 'passAllFeatures', we are done for this feature
 | ||||||
|                            break |                             break | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 if(!foundALayer){ |                 if (!foundALayer) { | ||||||
|                     noLayerFound.push(f) |                     noLayerFound.push(f) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  | @ -61,11 +60,11 @@ export default class PerLayerFeatureSourceSplitter { | ||||||
|             // At this point, we have our features per layer as a list
 |             // At this point, we have our features per layer as a list
 | ||||||
|             // We assign them to the correct featureSources
 |             // We assign them to the correct featureSources
 | ||||||
|             for (const layer of layers.data) { |             for (const layer of layers.data) { | ||||||
|                 const id = layer.layerDef.id; |                 const id = layer.layerDef.id | ||||||
|                 const features = featuresPerLayer.get(id) |                 const features = featuresPerLayer.get(id) | ||||||
|                 if (features === undefined) { |                 if (features === undefined) { | ||||||
|                     // No such features for this layer
 |                     // No such features for this layer
 | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 let featureSource = knownLayers.get(id) |                 let featureSource = knownLayers.get(id) | ||||||
|  | @ -86,7 +85,7 @@ export default class PerLayerFeatureSourceSplitter { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         layers.addCallback(_ => update()) |         layers.addCallback((_) => update()) | ||||||
|         upstream.features.addCallbackAndRunD(_ => update()) |         upstream.features.addCallbackAndRunD((_) => update()) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,52 +1,52 @@ | ||||||
| /** | /** | ||||||
|  * Applies geometry changes from 'Changes' onto every feature of a featureSource |  * Applies geometry changes from 'Changes' onto every feature of a featureSource | ||||||
|  */ |  */ | ||||||
| import {Changes} from "../../Osm/Changes"; | import { Changes } from "../../Osm/Changes" | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import { UIEventSource } from "../../UIEventSource" | ||||||
| import {FeatureSourceForLayer, IndexedFeatureSource} from "../FeatureSource"; | import { FeatureSourceForLayer, IndexedFeatureSource } from "../FeatureSource" | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import {ChangeDescription, ChangeDescriptionTools} from "../../Osm/Actions/ChangeDescription"; | import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| export default class ChangeGeometryApplicator implements FeatureSourceForLayer { | export default class ChangeGeometryApplicator implements FeatureSourceForLayer { | ||||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); |     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = | ||||||
|     public readonly name: string; |         new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||||
|  |     public readonly name: string | ||||||
|     public readonly layer: FilteredLayer |     public readonly layer: FilteredLayer | ||||||
|     private readonly source: IndexedFeatureSource; |     private readonly source: IndexedFeatureSource | ||||||
|     private readonly changes: Changes; |     private readonly changes: Changes | ||||||
| 
 | 
 | ||||||
|     constructor(source: (IndexedFeatureSource & FeatureSourceForLayer), changes: Changes) { |     constructor(source: IndexedFeatureSource & FeatureSourceForLayer, changes: Changes) { | ||||||
|         this.source = source; |         this.source = source | ||||||
|         this.changes = changes; |         this.changes = changes | ||||||
|         this.layer = source.layer |         this.layer = source.layer | ||||||
| 
 | 
 | ||||||
|         this.name = "ChangesApplied(" + source.name + ")" |         this.name = "ChangesApplied(" + source.name + ")" | ||||||
|         this.features = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined) |         this.features = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined) | ||||||
| 
 | 
 | ||||||
|         const self = this; |         const self = this | ||||||
|         source.features.addCallbackAndRunD(_ => self.update()) |         source.features.addCallbackAndRunD((_) => self.update()) | ||||||
| 
 |  | ||||||
|         changes.allChanges.addCallbackAndRunD(_ => self.update()) |  | ||||||
| 
 | 
 | ||||||
|  |         changes.allChanges.addCallbackAndRunD((_) => self.update()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private update() { |     private update() { | ||||||
|         const upstreamFeatures = this.source.features.data |         const upstreamFeatures = this.source.features.data | ||||||
|         const upstreamIds = this.source.containedIds.data |         const upstreamIds = this.source.containedIds.data | ||||||
|         const changesToApply = this.changes.allChanges.data |         const changesToApply = this.changes.allChanges.data?.filter( | ||||||
|             ?.filter(ch => |             (ch) => | ||||||
|                 // Does upsteram have this element? If not, we skip
 |                 // Does upsteram have this element? If not, we skip
 | ||||||
|                 upstreamIds.has(ch.type + "/" + ch.id) && |                 upstreamIds.has(ch.type + "/" + ch.id) && | ||||||
|                 // Are any (geometry) changes defined?
 |                 // Are any (geometry) changes defined?
 | ||||||
|                 ch.changes !== undefined && |                 ch.changes !== undefined && | ||||||
|                 // Ignore new elements, they are handled by the NewGeometryFromChangesFeatureSource
 |                 // Ignore new elements, they are handled by the NewGeometryFromChangesFeatureSource
 | ||||||
|                 ch.id > 0) |                 ch.id > 0 | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         if (changesToApply === undefined || changesToApply.length === 0) { |         if (changesToApply === undefined || changesToApply.length === 0) { | ||||||
|             // No changes to apply!
 |             // No changes to apply!
 | ||||||
|             // Pass the original feature and lets continue our day
 |             // Pass the original feature and lets continue our day
 | ||||||
|             this.features.setData(upstreamFeatures); |             this.features.setData(upstreamFeatures) | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const changesPerId = new Map<string, ChangeDescription[]>() |         const changesPerId = new Map<string, ChangeDescription[]>() | ||||||
|  | @ -58,27 +58,32 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer { | ||||||
|                 changesPerId.set(key, [ch]) |                 changesPerId.set(key, [ch]) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         const newFeatures: { feature: any, freshness: Date }[] = [] |         const newFeatures: { feature: any; freshness: Date }[] = [] | ||||||
|         for (const feature of upstreamFeatures) { |         for (const feature of upstreamFeatures) { | ||||||
|             const changesForFeature = changesPerId.get(feature.feature.properties.id) |             const changesForFeature = changesPerId.get(feature.feature.properties.id) | ||||||
|             if (changesForFeature === undefined) { |             if (changesForFeature === undefined) { | ||||||
|                 // No changes for this element
 |                 // No changes for this element
 | ||||||
|                 newFeatures.push(feature) |                 newFeatures.push(feature) | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Allright! We have a feature to rewrite!
 |             // Allright! We have a feature to rewrite!
 | ||||||
|             const copy = { |             const copy = { | ||||||
|                 ...feature |                 ...feature, | ||||||
|             } |             } | ||||||
|             // We only apply the last change as that one'll have the latest geometry
 |             // We only apply the last change as that one'll have the latest geometry
 | ||||||
|             const change = changesForFeature[changesForFeature.length - 1] |             const change = changesForFeature[changesForFeature.length - 1] | ||||||
|             copy.feature.geometry = ChangeDescriptionTools.getGeojsonGeometry(change) |             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) |             newFeatures.push(copy) | ||||||
|         } |         } | ||||||
|         this.features.setData(newFeatures) |         this.features.setData(newFeatures) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,99 +1,112 @@ | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import { UIEventSource } from "../../UIEventSource" | ||||||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; | import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import {Tiles} from "../../../Models/TileRange"; | import { Tiles } from "../../../Models/TileRange" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| 
 | 
 | ||||||
| 
 | export default class FeatureSourceMerger | ||||||
| export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource { |     implements FeatureSourceForLayer, Tiled, IndexedFeatureSource | ||||||
| 
 | { | ||||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); |     public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource< | ||||||
|     public readonly name; |         { feature: any; freshness: Date }[] | ||||||
|  |     >([]) | ||||||
|  |     public readonly name | ||||||
|     public readonly layer: FilteredLayer |     public readonly layer: FilteredLayer | ||||||
|     public readonly tileIndex: number; |     public readonly tileIndex: number | ||||||
|     public readonly bbox: BBox; |     public readonly bbox: BBox | ||||||
|     public readonly containedIds: UIEventSource<Set<string>> = new UIEventSource<Set<string>>(new Set()) |     public readonly containedIds: UIEventSource<Set<string>> = new UIEventSource<Set<string>>( | ||||||
|     private readonly _sources: UIEventSource<FeatureSource[]>; |         new Set() | ||||||
|  |     ) | ||||||
|  |     private readonly _sources: UIEventSource<FeatureSource[]> | ||||||
|     /** |     /** | ||||||
|      * Merges features from different featureSources for a single layer |      * 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 |      * 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[]>) { |     constructor( | ||||||
|         this.tileIndex = tileIndex; |         layer: FilteredLayer, | ||||||
|         this.bbox = bbox; |         tileIndex: number, | ||||||
|         this._sources = sources; |         bbox: BBox, | ||||||
|         this.layer = layer; |         sources: UIEventSource<FeatureSource[]> | ||||||
|         this.name = "FeatureSourceMerger(" + layer.layerDef.id + ", " + Tiles.tile_from_index(tileIndex).join(",") + ")" |     ) { | ||||||
|         const self = this; |         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 => { |         sources.addCallbackAndRunD((sources) => { | ||||||
|             let newSourceRegistered = false; |             let newSourceRegistered = false | ||||||
|             for (let i = 0; i < sources.length; i++) { |             for (let i = 0; i < sources.length; i++) { | ||||||
|                 let source = sources[i]; |                 let source = sources[i] | ||||||
|                 if (handledSources.has(source)) { |                 if (handledSources.has(source)) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|                 handledSources.add(source) |                 handledSources.add(source) | ||||||
|                 newSourceRegistered = true |                 newSourceRegistered = true | ||||||
|                 source.features.addCallback(() => { |                 source.features.addCallback(() => { | ||||||
|                     self.Update(); |                     self.Update() | ||||||
|                 }); |                 }) | ||||||
|                 if (newSourceRegistered) { |                 if (newSourceRegistered) { | ||||||
|                     self.Update(); |                     self.Update() | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private Update() { |     private Update() { | ||||||
| 
 |         let somethingChanged = false | ||||||
|         let somethingChanged = false; |         const all: Map<string, { feature: any; freshness: Date }> = new Map< | ||||||
|         const all: Map<string, { feature: any, freshness: Date }> = new Map<string, { feature: any; freshness: Date }>(); |             string, | ||||||
|  |             { feature: any; freshness: Date } | ||||||
|  |         >() | ||||||
|         // We seed the dictionary with the previously loaded features
 |         // We seed the dictionary with the previously loaded features
 | ||||||
|         const oldValues = this.features.data ?? []; |         const oldValues = this.features.data ?? [] | ||||||
|         for (const oldValue of oldValues) { |         for (const oldValue of oldValues) { | ||||||
|             all.set(oldValue.feature.id, oldValue) |             all.set(oldValue.feature.id, oldValue) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         for (const source of this._sources.data) { |         for (const source of this._sources.data) { | ||||||
|             if (source?.features?.data === undefined) { |             if (source?.features?.data === undefined) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
|             for (const f of source.features.data) { |             for (const f of source.features.data) { | ||||||
|                 const id = f.feature.properties.id; |                 const id = f.feature.properties.id | ||||||
|                 if (!all.has(id)) { |                 if (!all.has(id)) { | ||||||
|                     // This is a new feature
 |                     // This is a new feature
 | ||||||
|                     somethingChanged = true; |                     somethingChanged = true | ||||||
|                     all.set(id, f); |                     all.set(id, f) | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // This value has been seen already, either in a previous run or by a previous datasource
 |                 // This value has been seen already, either in a previous run or by a previous datasource
 | ||||||
|                 // Let's figure out if something changed
 |                 // Let's figure out if something changed
 | ||||||
|                 const oldV = all.get(id); |                 const oldV = all.get(id) | ||||||
|                 if (oldV.freshness < f.freshness) { |                 if (oldV.freshness < f.freshness) { | ||||||
|                     // Jup, this feature is fresher
 |                     // Jup, this feature is fresher
 | ||||||
|                     all.set(id, f); |                     all.set(id, f) | ||||||
|                     somethingChanged = true; |                     somethingChanged = true | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!somethingChanged) { |         if (!somethingChanged) { | ||||||
|             // We don't bother triggering an update
 |             // We don't bother triggering an update
 | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const newList = []; |         const newList = [] | ||||||
|         all.forEach((value, _) => { |         all.forEach((value, _) => { | ||||||
|             newList.push(value) |             newList.push(value) | ||||||
|         }) |         }) | ||||||
|         this.containedIds.setData(new Set(all.keys())) |         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 { Store, UIEventSource } from "../../UIEventSource" | ||||||
| import FilteredLayer, {FilterState} from "../../../Models/FilteredLayer"; | import FilteredLayer, { FilterState } from "../../../Models/FilteredLayer" | ||||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| import {ElementStorage} from "../../ElementStorage"; | import { ElementStorage } from "../../ElementStorage" | ||||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | import { TagsFilter } from "../../Tags/TagsFilter" | ||||||
| import {OsmFeature} from "../../../Models/OsmFeature"; | import { OsmFeature } from "../../../Models/OsmFeature" | ||||||
| 
 | 
 | ||||||
| export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled { | export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled { | ||||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = |     public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource< | ||||||
|         new UIEventSource<{ feature: any; freshness: Date }[]>([]); |         { feature: any; freshness: Date }[] | ||||||
|     public readonly name; |     >([]) | ||||||
|     public readonly layer: FilteredLayer; |     public readonly name | ||||||
|  |     public readonly layer: FilteredLayer | ||||||
|     public readonly tileIndex: number |     public readonly tileIndex: number | ||||||
|     public readonly bbox: BBox |     public readonly bbox: BBox | ||||||
|     private readonly upstream: FeatureSourceForLayer; |     private readonly upstream: FeatureSourceForLayer | ||||||
|     private readonly state: { |     private readonly state: { | ||||||
|         locationControl: Store<{ zoom: number }>;  |         locationControl: Store<{ zoom: number }> | ||||||
|         selectedElement: Store<any>, |         selectedElement: Store<any> | ||||||
|         globalFilters: Store<{ filter: FilterState }[]>, |         globalFilters: Store<{ filter: FilterState }[]> | ||||||
|         allElements: ElementStorage |         allElements: ElementStorage | ||||||
|     }; |     } | ||||||
|     private readonly _alreadyRegistered = new Set<UIEventSource<any>>(); |     private readonly _alreadyRegistered = new Set<UIEventSource<any>>() | ||||||
|     private readonly _is_dirty = new UIEventSource(false) |     private readonly _is_dirty = new UIEventSource(false) | ||||||
|     private previousFeatureSet: Set<any> = undefined; |     private previousFeatureSet: Set<any> = undefined | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         state: { |         state: { | ||||||
|             locationControl: Store<{ zoom: number }>, |             locationControl: Store<{ zoom: number }> | ||||||
|             selectedElement: Store<any>, |             selectedElement: Store<any> | ||||||
|             allElements: ElementStorage, |             allElements: ElementStorage | ||||||
|             globalFilters: Store<{ filter: FilterState }[]> |             globalFilters: Store<{ filter: FilterState }[]> | ||||||
|         }, |         }, | ||||||
|         tileIndex, |         tileIndex, | ||||||
|  | @ -41,92 +42,95 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti | ||||||
|         this.upstream = upstream |         this.upstream = upstream | ||||||
|         this.state = state |         this.state = state | ||||||
| 
 | 
 | ||||||
|         this.layer = upstream.layer; |         this.layer = upstream.layer | ||||||
|         const layer = upstream.layer; |         const layer = upstream.layer | ||||||
|         const self = this; |         const self = this | ||||||
|         upstream.features.addCallback(() => { |         upstream.features.addCallback(() => { | ||||||
|             self.update(); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         layer.appliedFilters.addCallback(_ => { |  | ||||||
|             self.update() |             self.update() | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         this._is_dirty.stabilized(1000).addCallbackAndRunD(dirty => { |         layer.appliedFilters.addCallback((_) => { | ||||||
|  |             self.update() | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         this._is_dirty.stabilized(1000).addCallbackAndRunD((dirty) => { | ||||||
|             if (dirty) { |             if (dirty) { | ||||||
|                 self.update() |                 self.update() | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         metataggingUpdated?.addCallback(_ => { |         metataggingUpdated?.addCallback((_) => { | ||||||
|             self._is_dirty.setData(true) |             self._is_dirty.setData(true) | ||||||
|         }) |         }) | ||||||
|          | 
 | ||||||
|         state.globalFilters.addCallback(_ => { |         state.globalFilters.addCallback((_) => { | ||||||
|             self.update() |             self.update() | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         this.update(); |         this.update() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private update() { |     private update() { | ||||||
|         const self = this; |         const self = this | ||||||
|         const layer = this.upstream.layer; |         const layer = this.upstream.layer | ||||||
|         const features: { feature: OsmFeature; freshness: Date }[] = (this.upstream.features.data ?? []); |         const features: { feature: OsmFeature; freshness: Date }[] = | ||||||
|         const includedFeatureIds = new Set<string>(); |             this.upstream.features.data ?? [] | ||||||
|         const globalFilters = self.state.globalFilters.data.map(f => f.filter); |         const includedFeatureIds = new Set<string>() | ||||||
|  |         const globalFilters = self.state.globalFilters.data.map((f) => f.filter) | ||||||
|         const newFeatures = (features ?? []).filter((f) => { |         const newFeatures = (features ?? []).filter((f) => { | ||||||
| 
 |  | ||||||
|             self.registerCallback(f.feature) |             self.registerCallback(f.feature) | ||||||
| 
 | 
 | ||||||
|             const isShown: TagsFilter = layer.layerDef.isShown; |             const isShown: TagsFilter = layer.layerDef.isShown | ||||||
|             const tags = f.feature.properties; |             const tags = f.feature.properties | ||||||
|             if (isShown !== undefined && !isShown.matchesProperties(tags) ) { |             if (isShown !== undefined && !isShown.matchesProperties(tags)) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const tagsFilter = Array.from(layer.appliedFilters?.data?.values() ?? []) |             const tagsFilter = Array.from(layer.appliedFilters?.data?.values() ?? []) | ||||||
|             for (const filter of tagsFilter) { |             for (const filter of tagsFilter) { | ||||||
|                 const neededTags: TagsFilter = filter?.currentFilter |                 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
 |                     // 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) { |             for (const filter of globalFilters) { | ||||||
|                 const neededTags: TagsFilter = filter?.currentFilter |                 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
 |                     // 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) |             includedFeatureIds.add(f.feature.properties.id) | ||||||
|             return true; |             return true | ||||||
|         }); |         }) | ||||||
| 
 | 
 | ||||||
|         const previousSet = this.previousFeatureSet; |         const previousSet = this.previousFeatureSet | ||||||
|         this._is_dirty.setData(false) |         this._is_dirty.setData(false) | ||||||
| 
 | 
 | ||||||
|         // Is there any difference between the two sets?
 |         // Is there any difference between the two sets?
 | ||||||
|         if (previousSet !== undefined && previousSet.size === includedFeatureIds.size) { |         if (previousSet !== undefined && previousSet.size === includedFeatureIds.size) { | ||||||
|             // The size of the sets is the same - they _might_ be identical
 |             // 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) { |             if (!newItemFound) { | ||||||
|                 // We know that: 
 |                 // We know that:
 | ||||||
|                 // - The sets have the same size
 |                 // - The sets have the same size
 | ||||||
|                 // - Every item from the new set has been found in the old set
 |                 // - Every item from the new set has been found in the old set
 | ||||||
|                 // which means they are identical!
 |                 // which means they are identical!
 | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Something new has been found!
 |         // Something new has been found!
 | ||||||
|         this.features.setData(newFeatures); |         this.features.setData(newFeatures) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private registerCallback(feature: any) { |     private registerCallback(feature: any) { | ||||||
|  | @ -139,11 +143,10 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti | ||||||
|         } |         } | ||||||
|         this._alreadyRegistered.add(src) |         this._alreadyRegistered.add(src) | ||||||
| 
 | 
 | ||||||
|         const self = this; |         const self = this | ||||||
|         // Add a callback as a changed tag migh change the filter
 |         // Add a callback as a changed tag migh change the filter
 | ||||||
|         src.addCallbackAndRunD(_ => { |         src.addCallbackAndRunD((_) => { | ||||||
|             self._is_dirty.setData(true) |             self._is_dirty.setData(true) | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,168 +1,163 @@ | ||||||
| /** | /** | ||||||
|  * Fetches a geojson file somewhere and passes it along |  * Fetches a geojson file somewhere and passes it along | ||||||
|  */ |  */ | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import { UIEventSource } from "../../UIEventSource" | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import {Utils} from "../../../Utils"; | import { Utils } from "../../../Utils" | ||||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||||
| import {Tiles} from "../../../Models/TileRange"; | import { Tiles } from "../../../Models/TileRange" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| import {GeoOperations} from "../../GeoOperations"; | import { GeoOperations } from "../../GeoOperations" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | ||||||
| 
 |     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> | ||||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; |     public readonly state = new UIEventSource<undefined | { error: string } | "loaded">(undefined) | ||||||
|     public readonly state = new UIEventSource<undefined | {error: string} | "loaded">(undefined) |     public readonly name | ||||||
|     public readonly name; |  | ||||||
|     public readonly isOsmCache: boolean |     public readonly isOsmCache: boolean | ||||||
|     public readonly layer: FilteredLayer; |     public readonly layer: FilteredLayer | ||||||
|     public readonly tileIndex |     public readonly tileIndex | ||||||
|     public readonly bbox; |     public readonly bbox | ||||||
|     private readonly seenids: Set<string>; |     private readonly seenids: Set<string> | ||||||
|     private readonly idKey ?: string; |     private readonly idKey?: string | ||||||
| 
 |  | ||||||
|     public constructor(flayer: FilteredLayer, |  | ||||||
|                        zxy?: [number, number, number] | BBox, |  | ||||||
|                        options?: { |  | ||||||
|                            featureIdBlacklist?: Set<string> |  | ||||||
|                        }) { |  | ||||||
| 
 | 
 | ||||||
|  |     public constructor( | ||||||
|  |         flayer: FilteredLayer, | ||||||
|  |         zxy?: [number, number, number] | BBox, | ||||||
|  |         options?: { | ||||||
|  |             featureIdBlacklist?: Set<string> | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|         if (flayer.layerDef.source.geojsonZoomLevel !== undefined && zxy === undefined) { |         if (flayer.layerDef.source.geojsonZoomLevel !== undefined && zxy === undefined) { | ||||||
|             throw "Dynamic layers are not supported. Use 'DynamicGeoJsonTileSource instead" |             throw "Dynamic layers are not supported. Use 'DynamicGeoJsonTileSource instead" | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.layer = flayer; |         this.layer = flayer | ||||||
|         this.idKey = flayer.layerDef.source.idKey |         this.idKey = flayer.layerDef.source.idKey | ||||||
|         this.seenids = options?.featureIdBlacklist ?? new Set<string>() |         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) { |         if (zxy !== undefined) { | ||||||
|             let tile_bbox: BBox; |             let tile_bbox: BBox | ||||||
|             if (zxy instanceof BBox) { |             if (zxy instanceof BBox) { | ||||||
|                 tile_bbox = zxy; |                 tile_bbox = zxy | ||||||
|             } else { |             } else { | ||||||
|                 const [z, x, y] = zxy; |                 const [z, x, y] = zxy | ||||||
|                 tile_bbox = BBox.fromTile(z, x, y); |                 tile_bbox = BBox.fromTile(z, x, y) | ||||||
| 
 | 
 | ||||||
|                 this.tileIndex = Tiles.tile_index(z, x, y) |                 this.tileIndex = Tiles.tile_index(z, x, y) | ||||||
|                 this.bbox = BBox.fromTile(z, x, y) |                 this.bbox = BBox.fromTile(z, x, y) | ||||||
|                 url = url |                 url = url | ||||||
|                     .replace('{z}', "" + z) |                     .replace("{z}", "" + z) | ||||||
|                     .replace('{x}', "" + x) |                     .replace("{x}", "" + x) | ||||||
|                     .replace('{y}', "" + y) |                     .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) { |             if (this.layer.layerDef.source.mercatorCrs) { | ||||||
|                 bounds = tile_bbox.toMercator() |                 bounds = tile_bbox.toMercator() | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             url = url |             url = url | ||||||
|                 .replace('{y_min}', "" + bounds.minLat) |                 .replace("{y_min}", "" + bounds.minLat) | ||||||
|                 .replace('{y_max}', "" + bounds.maxLat) |                 .replace("{y_max}", "" + bounds.maxLat) | ||||||
|                 .replace('{x_min}', "" + bounds.minLon) |                 .replace("{x_min}", "" + bounds.minLon) | ||||||
|                 .replace('{x_max}', "" + bounds.maxLon) |                 .replace("{x_max}", "" + bounds.maxLon) | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         } else { |         } else { | ||||||
|             this.tileIndex = Tiles.tile_index(0, 0, 0) |             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.features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||||
|         this.LoadJSONFrom(url) |         this.LoadJSONFrom(url) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     private LoadJSONFrom(url: string) { |     private LoadJSONFrom(url: string) { | ||||||
|         const eventSource = this.features; |         const eventSource = this.features | ||||||
|         const self = this; |         const self = this | ||||||
|         Utils.downloadJsonCached(url, 60 * 60) |         Utils.downloadJsonCached(url, 60 * 60) | ||||||
|             .then(json => { |             .then((json) => { | ||||||
|                 self.state.setData("loaded") |                 self.state.setData("loaded") | ||||||
|                 // TODO: move somewhere else, just for testing
 |                 // TODO: move somewhere else, just for testing
 | ||||||
|                 // Check for maproulette data
 |                 // Check for maproulette data
 | ||||||
|                 if (url.startsWith("https://maproulette.org/api/v2/tasks/box/")) { |                 if (url.startsWith("https://maproulette.org/api/v2/tasks/box/")) { | ||||||
|                     console.log("MapRoulette data detected") |                     console.log("MapRoulette data detected") | ||||||
|                     const data = json; |                     const data = json | ||||||
|                     let maprouletteFeatures: any[] = []; |                     let maprouletteFeatures: any[] = [] | ||||||
|                     data.forEach(element => { |                     data.forEach((element) => { | ||||||
|                         maprouletteFeatures.push({ |                         maprouletteFeatures.push({ | ||||||
|                             type: "Feature", |                             type: "Feature", | ||||||
|                             geometry: { |                             geometry: { | ||||||
|                                 type: "Point", |                                 type: "Point", | ||||||
|                                 coordinates: [element.point.lng, element.point.lat] |                                 coordinates: [element.point.lng, element.point.lat], | ||||||
|                             }, |                             }, | ||||||
|                             properties: { |                             properties: { | ||||||
|                                 // Map all properties to the feature
 |                                 // Map all properties to the feature
 | ||||||
|                                 ...element, |                                 ...element, | ||||||
|                             } |                             }, | ||||||
|                         }); |                         }) | ||||||
|                     }); |                     }) | ||||||
|                     json.features = maprouletteFeatures; |                     json.features = maprouletteFeatures | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (json.features === undefined || json.features === null) { |                 if (json.features === undefined || json.features === null) { | ||||||
|                     return; |                     return | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (self.layer.layerDef.source.mercatorCrs) { |                 if (self.layer.layerDef.source.mercatorCrs) { | ||||||
|                     json = GeoOperations.GeoJsonToWGS84(json) |                     json = GeoOperations.GeoJsonToWGS84(json) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 const time = new Date(); |                 const time = new Date() | ||||||
|                 const newFeatures: { feature: any, freshness: Date } [] = [] |                 const newFeatures: { feature: any; freshness: Date }[] = [] | ||||||
|                 let i = 0; |                 let i = 0 | ||||||
|                 let skipped = 0; |                 let skipped = 0 | ||||||
|                 for (const feature of json.features) { |                 for (const feature of json.features) { | ||||||
|                     const props = feature.properties |                     const props = feature.properties | ||||||
|                     for (const key in props) { |                     for (const key in props) { | ||||||
|                          |                         if (props[key] === null) { | ||||||
|                         if(props[key] === null){ |  | ||||||
|                             delete props[key] |                             delete props[key] | ||||||
|                         } |                         } | ||||||
|                          | 
 | ||||||
|                         if (typeof props[key] !== "string") { |                         if (typeof props[key] !== "string") { | ||||||
|                             // Make sure all the values are string, it crashes stuff otherwise
 |                             // Make sure all the values are string, it crashes stuff otherwise
 | ||||||
|                             props[key] = JSON.stringify(props[key]) |                             props[key] = JSON.stringify(props[key]) | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     if(self.idKey !== undefined){ |                     if (self.idKey !== undefined) { | ||||||
|                         props.id = props[self.idKey] |                         props.id = props[self.idKey] | ||||||
|                     } |                     } | ||||||
|                      | 
 | ||||||
|                     if (props.id === undefined) { |                     if (props.id === undefined) { | ||||||
|                         props.id = url + "/" + i; |                         props.id = url + "/" + i | ||||||
|                         feature.id = url + "/" + i; |                         feature.id = url + "/" + i | ||||||
|                         i++; |                         i++ | ||||||
|                     } |                     } | ||||||
|                     if (self.seenids.has(props.id)) { |                     if (self.seenids.has(props.id)) { | ||||||
|                         skipped++; |                         skipped++ | ||||||
|                         continue; |                         continue | ||||||
|                     } |                     } | ||||||
|                     self.seenids.add(props.id) |                     self.seenids.add(props.id) | ||||||
| 
 | 
 | ||||||
|                     let freshness: Date = time; |                     let freshness: Date = time | ||||||
|                     if (feature.properties["_last_edit:timestamp"] !== undefined) { |                     if (feature.properties["_last_edit:timestamp"] !== undefined) { | ||||||
|                         freshness = new Date(props["_last_edit:timestamp"]) |                         freshness = new Date(props["_last_edit:timestamp"]) | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     newFeatures.push({feature: feature, freshness: freshness}) |                     newFeatures.push({ feature: feature, freshness: freshness }) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (newFeatures.length == 0) { |                 if (newFeatures.length == 0) { | ||||||
|                     return; |                     return | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 eventSource.setData(eventSource.data.concat(newFeatures)) |                 eventSource.setData(eventSource.data.concat(newFeatures)) | ||||||
| 
 |             }) | ||||||
|             }).catch(msg => { |             .catch((msg) => { | ||||||
|             console.debug("Could not load geojson layer", url, "due to", msg); |                 console.debug("Could not load geojson layer", url, "due to", msg) | ||||||
|             self.state.setData({error: msg}) |                 self.state.setData({ error: msg }) | ||||||
|         }) |             }) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,50 +1,50 @@ | ||||||
| import {Changes} from "../../Osm/Changes"; | import { Changes } from "../../Osm/Changes" | ||||||
| import {OsmNode, OsmObject, OsmRelation, OsmWay} from "../../Osm/OsmObject"; | import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject" | ||||||
| import FeatureSource from "../FeatureSource"; | import FeatureSource from "../FeatureSource" | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import { UIEventSource } from "../../UIEventSource" | ||||||
| import {ChangeDescription} from "../../Osm/Actions/ChangeDescription"; | import { ChangeDescription } from "../../Osm/Actions/ChangeDescription" | ||||||
| import {ElementStorage} from "../../ElementStorage"; | import { ElementStorage } from "../../ElementStorage" | ||||||
| import {OsmId, OsmTags} from "../../../Models/OsmFeature"; | import { OsmId, OsmTags } from "../../../Models/OsmFeature" | ||||||
| 
 | 
 | ||||||
| export class NewGeometryFromChangesFeatureSource implements FeatureSource { | export class NewGeometryFromChangesFeatureSource implements FeatureSource { | ||||||
|     // This class name truly puts the 'Java' into 'Javascript'
 |     // This class name truly puts the 'Java' into 'Javascript'
 | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * A feature source containing exclusively new elements. |      * A feature source containing exclusively new elements. | ||||||
|      *  |      * | ||||||
|      * These elements are probably created by the 'SimpleAddUi' which generates a new point, but the import functionality might create a line or polygon too. |      * 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 |      * 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 features: UIEventSource<{ feature: any; freshness: Date }[]> = | ||||||
|     public readonly name: string = "newFeatures"; |         new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||||
|  |     public readonly name: string = "newFeatures" | ||||||
| 
 | 
 | ||||||
|     constructor(changes: Changes, allElementStorage: ElementStorage, backendUrl: string) { |     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>(); |         changes.pendingChanges.stabilized(100).addCallbackAndRunD((changes) => { | ||||||
|         const features = this.features.data; |  | ||||||
|         const self = this; |  | ||||||
| 
 |  | ||||||
|         changes.pendingChanges.stabilized(100).addCallbackAndRunD(changes => { |  | ||||||
|             if (changes.length === 0) { |             if (changes.length === 0) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const now = new Date(); |             const now = new Date() | ||||||
|             let somethingChanged = false; |             let somethingChanged = false | ||||||
| 
 | 
 | ||||||
|             function add(feature) { |             function add(feature) { | ||||||
|                 feature.id = feature.properties.id |                 feature.id = feature.properties.id | ||||||
|                 features.push({ |                 features.push({ | ||||||
|                     feature: feature, |                     feature: feature, | ||||||
|                     freshness: now |                     freshness: now, | ||||||
|                 }) |                 }) | ||||||
|                 somethingChanged = true; |                 somethingChanged = true | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             for (const change of changes) { |             for (const change of changes) { | ||||||
|                 if (seenChanges.has(change)) { |                 if (seenChanges.has(change)) { | ||||||
|                     // Already handled
 |                     // Already handled
 | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
|                 seenChanges.add(change) |                 seenChanges.add(change) | ||||||
| 
 | 
 | ||||||
|  | @ -60,35 +60,32 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { | ||||||
|                     // For this, we introspect the change
 |                     // For this, we introspect the change
 | ||||||
|                     if (allElementStorage.has(change.type + "/" + change.id)) { |                     if (allElementStorage.has(change.type + "/" + change.id)) { | ||||||
|                         // The current point already exists, we don't have to do anything here
 |                         // The current point already exists, we don't have to do anything here
 | ||||||
|                         continue; |                         continue | ||||||
|                     } |                     } | ||||||
|                     console.debug("Detected a reused point") |                     console.debug("Detected a reused point") | ||||||
|                     // The 'allElementsStore' does _not_ have this point yet, so we have to create it
 |                     // 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) |                         console.log("Got the reused point:", feat) | ||||||
|                         for (const kv of change.tags) { |                         for (const kv of change.tags) { | ||||||
|                             feat.tags[kv.k] = kv.v |                             feat.tags[kv.k] = kv.v | ||||||
|                         } |                         } | ||||||
|                         const geojson = feat.asGeoJson(); |                         const geojson = feat.asGeoJson() | ||||||
|                         allElementStorage.addOrGetElement(geojson) |                         allElementStorage.addOrGetElement(geojson) | ||||||
|                         self.features.data.push({feature: geojson, freshness: new Date()}) |                         self.features.data.push({ feature: geojson, freshness: new Date() }) | ||||||
|                         self.features.ping() |                         self.features.ping() | ||||||
|                     }) |                     }) | ||||||
|                     continue |                     continue | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                 } else if (change.id < 0 && change.changes === undefined) { |                 } else if (change.id < 0 && change.changes === undefined) { | ||||||
|                     // The geometry is not described - not a new point
 |                     // The geometry is not described - not a new point
 | ||||||
|                     if (change.id < 0) { |                     if (change.id < 0) { | ||||||
|                         console.error("WARNING: got a new point without geometry!") |                         console.error("WARNING: got a new point without geometry!") | ||||||
|                     } |                     } | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|                 try { |                 try { | ||||||
|                     const tags: OsmTags = { |                     const tags: OsmTags = { | ||||||
|                         id: <OsmId> (change.type + "/" + change.id) |                         id: <OsmId>(change.type + "/" + change.id), | ||||||
|                     } |                     } | ||||||
|                     for (const kv of change.tags) { |                     for (const kv of change.tags) { | ||||||
|                         tags[kv.k] = kv.v |                         tags[kv.k] = kv.v | ||||||
|  | @ -104,30 +101,31 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { | ||||||
|                             n.lon = change.changes["lon"] |                             n.lon = change.changes["lon"] | ||||||
|                             const geojson = n.asGeoJson() |                             const geojson = n.asGeoJson() | ||||||
|                             add(geojson) |                             add(geojson) | ||||||
|                             break; |                             break | ||||||
|                         case "way": |                         case "way": | ||||||
|                             const w = new OsmWay(change.id) |                             const w = new OsmWay(change.id) | ||||||
|                             w.tags = tags |                             w.tags = tags | ||||||
|                             w.nodes = change.changes["nodes"] |                             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()) |                             add(w.asGeoJson()) | ||||||
|                             break; |                             break | ||||||
|                         case "relation": |                         case "relation": | ||||||
|                             const r = new OsmRelation(change.id) |                             const r = new OsmRelation(change.id) | ||||||
|                             r.tags = tags |                             r.tags = tags | ||||||
|                             r.members = change.changes["members"] |                             r.members = change.changes["members"] | ||||||
|                             add(r.asGeoJson()) |                             add(r.asGeoJson()) | ||||||
|                             break; |                             break | ||||||
|                     } |                     } | ||||||
|                 } catch (e) { |                 } catch (e) { | ||||||
|                     console.error("Could not generate a new geometry to render on screen for:", e) |                     console.error("Could not generate a new geometry to render on screen for:", e) | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|             if (somethingChanged) { |             if (somethingChanged) { | ||||||
|                 self.features.ping() |                 self.features.ping() | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -2,34 +2,36 @@ | ||||||
|  * Every previously added point is remembered, but new points are added. |  * Every previously added point is remembered, but new points are added. | ||||||
|  * Data coming from upstream will always overwrite a previous value |  * Data coming from upstream will always overwrite a previous value | ||||||
|  */ |  */ | ||||||
| import FeatureSource, {Tiled} from "../FeatureSource"; | import FeatureSource, { Tiled } from "../FeatureSource" | ||||||
| import {Store, UIEventSource} from "../../UIEventSource"; | import { Store, UIEventSource } from "../../UIEventSource" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| 
 | 
 | ||||||
| export default class RememberingSource implements FeatureSource, Tiled { | export default class RememberingSource implements FeatureSource, Tiled { | ||||||
| 
 |     public readonly features: Store<{ feature: any; freshness: Date }[]> | ||||||
|     public readonly features: Store<{ feature: any, freshness: Date }[]>; |     public readonly name | ||||||
|     public readonly name; |  | ||||||
|     public readonly tileIndex: number |     public readonly tileIndex: number | ||||||
|     public readonly bbox: BBox |     public readonly bbox: BBox | ||||||
| 
 | 
 | ||||||
|     constructor(source: FeatureSource & Tiled) { |     constructor(source: FeatureSource & Tiled) { | ||||||
|         const self = this; |         const self = this | ||||||
|         this.name = "RememberingSource of " + source.name; |         this.name = "RememberingSource of " + source.name | ||||||
|         this.tileIndex = source.tileIndex |         this.tileIndex = source.tileIndex | ||||||
|         this.bbox = source.bbox; |         this.bbox = source.bbox | ||||||
| 
 | 
 | ||||||
|         const empty = []; |         const empty = [] | ||||||
|         const featureSource = new UIEventSource<{feature: any, freshness: Date}[]>(empty) |         const featureSource = new UIEventSource<{ feature: any; freshness: Date }[]>(empty) | ||||||
|         this.features = featureSource |         this.features = featureSource | ||||||
|         source.features.addCallbackAndRunD(features => { |         source.features.addCallbackAndRunD((features) => { | ||||||
|             const oldFeatures = self.features?.data ?? empty; |             const oldFeatures = self.features?.data ?? empty | ||||||
|             // Then new ids
 |             // 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
 |             // 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]) |             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. |  * 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 { Store } from "../../UIEventSource" | ||||||
| import {GeoOperations} from "../../GeoOperations"; | import { GeoOperations } from "../../GeoOperations" | ||||||
| import FeatureSource from "../FeatureSource"; | import FeatureSource from "../FeatureSource" | ||||||
| import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig"; | import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig" | ||||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||||
| import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig"; | import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig" | ||||||
| 
 | 
 | ||||||
| export default class RenderingMultiPlexerFeatureSource { | export default class RenderingMultiPlexerFeatureSource { | ||||||
|     public readonly features: Store<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>; |     public readonly features: Store< | ||||||
|     private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[]; |         (any & { | ||||||
|     private centroidRenderings: { rendering: PointRenderingConfig; index: number }[]; |             pointRenderingIndex: number | undefined | ||||||
|     private projectedCentroidRenderings: { rendering: PointRenderingConfig; index: number }[]; |             lineRenderingIndex: number | undefined | ||||||
|     private startRenderings: { rendering: PointRenderingConfig; index: number }[]; |         })[] | ||||||
|     private endRenderings: { rendering: PointRenderingConfig; index: number }[]; |     > | ||||||
|     private hasCentroid: boolean; |     private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[] | ||||||
|     private lineRenderObjects: LineRenderingConfig[]; |     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( | ||||||
|     private inspectFeature(feat, addAsPoint: (feat, rendering, centerpoint: [number, number]) => void, withIndex: any[]){ |         feat, | ||||||
|  |         addAsPoint: (feat, rendering, centerpoint: [number, number]) => void, | ||||||
|  |         withIndex: any[] | ||||||
|  |     ) { | ||||||
|         if (feat.geometry.type === "Point") { |         if (feat.geometry.type === "Point") { | ||||||
| 
 |  | ||||||
|             for (const rendering of this.pointRenderings) { |             for (const rendering of this.pointRenderings) { | ||||||
|                 withIndex.push({ |                 withIndex.push({ | ||||||
|                     ...feat, |                     ...feat, | ||||||
|                     pointRenderingIndex: rendering.index |                     pointRenderingIndex: rendering.index, | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             // This is a a line: add the centroids
 |             // This is a a line: add the centroids
 | ||||||
|             let centerpoint: [number, number] = undefined; |             let centerpoint: [number, number] = undefined | ||||||
|             let projectedCenterPoint : [number, number] = undefined |             let projectedCenterPoint: [number, number] = undefined | ||||||
|             if(this.hasCentroid){ |             if (this.hasCentroid) { | ||||||
|                 centerpoint  = GeoOperations.centerpointCoordinates(feat) |                 centerpoint = GeoOperations.centerpointCoordinates(feat) | ||||||
|                 if(this.projectedCentroidRenderings.length > 0){ |                 if (this.projectedCentroidRenderings.length > 0) { | ||||||
|                     projectedCenterPoint = <[number,number]> GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates |                     projectedCenterPoint = <[number, number]>( | ||||||
|  |                         GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates | ||||||
|  |                     ) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             for (const rendering of this.centroidRenderings) { |             for (const rendering of this.centroidRenderings) { | ||||||
|                 addAsPoint(feat, rendering, centerpoint) |                 addAsPoint(feat, rendering, centerpoint) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|             if (feat.geometry.type === "LineString") { |             if (feat.geometry.type === "LineString") { | ||||||
| 
 |  | ||||||
|                 for (const rendering of this.projectedCentroidRenderings) { |                 for (const rendering of this.projectedCentroidRenderings) { | ||||||
|                     addAsPoint(feat, rendering, projectedCenterPoint) |                     addAsPoint(feat, rendering, projectedCenterPoint) | ||||||
|                 } |                 } | ||||||
|  | @ -58,73 +65,69 @@ export default class RenderingMultiPlexerFeatureSource { | ||||||
|                     const coordinate = coordinates[coordinates.length - 1] |                     const coordinate = coordinates[coordinates.length - 1] | ||||||
|                     addAsPoint(feat, rendering, coordinate) |                     addAsPoint(feat, rendering, coordinate) | ||||||
|                 } |                 } | ||||||
| 
 |             } else { | ||||||
|             }else{ |  | ||||||
|                 for (const rendering of this.projectedCentroidRenderings) { |                 for (const rendering of this.projectedCentroidRenderings) { | ||||||
|                     addAsPoint(feat, rendering, centerpoint) |                     addAsPoint(feat, rendering, centerpoint) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // AT last, add it 'as is' to what we should render 
 |             // AT last, add it 'as is' to what we should render
 | ||||||
|             for (let i = 0; i < this.lineRenderObjects.length; i++) { |             for (let i = 0; i < this.lineRenderObjects.length; i++) { | ||||||
|                 withIndex.push({ |                 withIndex.push({ | ||||||
|                     ...feat, |                     ...feat, | ||||||
|                     lineRenderingIndex: i |                     lineRenderingIndex: i, | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     constructor(upstream: FeatureSource, layer: LayerConfig) { |     constructor(upstream: FeatureSource, layer: LayerConfig) { | ||||||
|          |         const pointRenderObjects: { rendering: PointRenderingConfig; index: number }[] = | ||||||
|         const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({ |             layer.mapRendering.map((r, i) => ({ | ||||||
|             rendering: r, |                 rendering: r, | ||||||
|             index: i |                 index: i, | ||||||
|         })) |             })) | ||||||
|         this.pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point")) |         this.pointRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("point")) | ||||||
|         this.centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid")) |         this.centroidRenderings = pointRenderObjects.filter((r) => | ||||||
|         this.projectedCentroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("projected_centerpoint")) |             r.rendering.location.has("centroid") | ||||||
|         this.startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start")) |         ) | ||||||
|         this.endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end")) |         this.projectedCentroidRenderings = pointRenderObjects.filter((r) => | ||||||
|         this.hasCentroid = this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0 |             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.lineRenderObjects = layer.lineRendering | ||||||
|          |  | ||||||
|         this.features = upstream.features.map( |  | ||||||
|             features => { |  | ||||||
|                 if (features === undefined) { |  | ||||||
|                     return undefined; |  | ||||||
|                 } |  | ||||||
| 
 | 
 | ||||||
| 
 |         this.features = upstream.features.map((features) => { | ||||||
|                 const withIndex: any[] = []; |             if (features === undefined) { | ||||||
| 
 |                 return undefined | ||||||
|                 function addAsPoint(feat, rendering, coordinate) { |  | ||||||
|                     const patched = { |  | ||||||
|                         ...feat, |  | ||||||
|                         pointRenderingIndex: rendering.index |  | ||||||
|                     } |  | ||||||
|                     patched.geometry = { |  | ||||||
|                         type: "Point", |  | ||||||
|                         coordinates: coordinate |  | ||||||
|                     } |  | ||||||
|                     withIndex.push(patched) |  | ||||||
|                 } |  | ||||||
|           |  | ||||||
| 
 |  | ||||||
|                 for (const f of features) { |  | ||||||
|                     const feat = f.feature; |  | ||||||
|                     if(feat === undefined){ |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
|                    this.inspectFeature(feat, addAsPoint, withIndex) |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                 return withIndex; |  | ||||||
|             } |             } | ||||||
|         ); |  | ||||||
| 
 | 
 | ||||||
|  |             const withIndex: any[] = [] | ||||||
|  | 
 | ||||||
|  |             function addAsPoint(feat, rendering, coordinate) { | ||||||
|  |                 const patched = { | ||||||
|  |                     ...feat, | ||||||
|  |                     pointRenderingIndex: rendering.index, | ||||||
|  |                 } | ||||||
|  |                 patched.geometry = { | ||||||
|  |                     type: "Point", | ||||||
|  |                     coordinates: coordinate, | ||||||
|  |                 } | ||||||
|  |                 withIndex.push(patched) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             for (const f of features) { | ||||||
|  |                 const feat = f.feature | ||||||
|  |                 if (feat === undefined) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 this.inspectFeature(feat, addAsPoint, withIndex) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return withIndex | ||||||
|  |         }) | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,21 +1,24 @@ | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import { UIEventSource } from "../../UIEventSource" | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| 
 | 
 | ||||||
| export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled { | export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled { | ||||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; |     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> | ||||||
|     public readonly name: string = "SimpleFeatureSource"; |     public readonly name: string = "SimpleFeatureSource" | ||||||
|     public readonly layer: FilteredLayer; |     public readonly layer: FilteredLayer | ||||||
|     public readonly bbox: BBox = BBox.global; |     public readonly bbox: BBox = BBox.global | ||||||
|     public readonly tileIndex: number; |     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.name = "SimpleFeatureSource(" + layer.layerDef.id + ")" | ||||||
|         this.layer = layer |         this.layer = layer | ||||||
|         this.tileIndex = tileIndex ?? 0; |         this.tileIndex = tileIndex ?? 0 | ||||||
|         this.bbox = BBox.fromTileIndex(this.tileIndex) |         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 FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||||
| import {ImmutableStore, Store, UIEventSource} from "../../UIEventSource"; | import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" | ||||||
| import {stat} from "fs"; | import { stat } from "fs" | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| import {Feature} from "@turf/turf"; | import { Feature } from "@turf/turf" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * A simple, read only feature store. |  * A simple, read only feature store. | ||||||
|  */ |  */ | ||||||
| export default class StaticFeatureSource implements FeatureSource { | 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 |     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) { |         if (features === undefined) { | ||||||
|             throw "Static feature source received undefined as source" |             throw "Static feature source received undefined as source" | ||||||
|         } |         } | ||||||
|         this.name = name; |         this.name = name | ||||||
|         this.features = features; |         this.features = features | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static fromGeojsonAndDate(features: { feature: Feature, freshness: Date }[], name = "StaticFeatureSourceFromGeojsonAndDate"): StaticFeatureSource { |     public static fromGeojsonAndDate( | ||||||
|         return new StaticFeatureSource(new ImmutableStore(features), name); |         features: { feature: Feature; freshness: Date }[], | ||||||
|  |         name = "StaticFeatureSourceFromGeojsonAndDate" | ||||||
|  |     ): StaticFeatureSource { | ||||||
|  |         return new StaticFeatureSource(new ImmutableStore(features), name) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |     public static fromGeojson( | ||||||
|     public static fromGeojson(geojson: Feature[], name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource { |         geojson: Feature[], | ||||||
|         const now = new Date(); |         name = "StaticFeatureSourceFromGeojson" | ||||||
|         return StaticFeatureSource.fromGeojsonAndDate(geojson.map(feature => ({feature, freshness: now})), name); |     ): StaticFeatureSource { | ||||||
|  |         const now = new Date() | ||||||
|  |         return StaticFeatureSource.fromGeojsonAndDate( | ||||||
|  |             geojson.map((feature) => ({ feature, freshness: now })), | ||||||
|  |             name | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static fromGeojsonStore(geojson: Store<Feature[]>, name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource { |     public static fromGeojsonStore( | ||||||
|         const now = new Date(); |         geojson: Store<Feature[]>, | ||||||
|         const mapped : Store<{feature: Feature, freshness: Date}[]> = geojson.map(features => features.map(feature => ({feature, freshness: now}))) |         name = "StaticFeatureSourceFromGeojson" | ||||||
|         return new StaticFeatureSource(mapped, name); |     ): 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") { |     static fromDateless( | ||||||
|         const now = new Date(); |         featureSource: Store<{ feature: Feature }[]>, | ||||||
|         return new StaticFeatureSource(featureSource.map(features => features.map(feature => ({ |         name = "StaticFeatureSourceFromDateless" | ||||||
|             feature: feature.feature, |     ) { | ||||||
|             freshness: now |         const now = new Date() | ||||||
|         }))), name); |         return new StaticFeatureSource( | ||||||
|  |             featureSource.map((features) => | ||||||
|  |                 features.map((feature) => ({ | ||||||
|  |                     feature: feature.feature, | ||||||
|  |                     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; |     constructor( | ||||||
|     public readonly tileIndex: number;    |         features: Store<{ feature: any; freshness: Date }[]>, | ||||||
|     public readonly layer: FilteredLayer; |         layer: FilteredLayer, | ||||||
| 
 |         tileIndex: number = 0 | ||||||
|     constructor(features: Store<{ feature: any, freshness: Date }[]>, layer: FilteredLayer ,tileIndex : number = 0) { |     ) { | ||||||
|         super(features); |         super(features) | ||||||
|         this.tileIndex = tileIndex ; |         this.tileIndex = tileIndex | ||||||
|         this.layer=  layer; |         this.layer = layer | ||||||
|         this.bbox = BBox.fromTileIndex(this.tileIndex) |         this.bbox = BBox.fromTileIndex(this.tileIndex) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,12 +1,11 @@ | ||||||
| import {Tiles} from "../../Models/TileRange"; | import { Tiles } from "../../Models/TileRange" | ||||||
| 
 | 
 | ||||||
| export default class TileFreshnessCalculator { | export default class TileFreshnessCalculator { | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * All the freshnesses per tile index |      * All the freshnesses per tile index | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private readonly freshnesses = new Map<number, Date>(); |     private readonly freshnesses = new Map<number, Date>() | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Marks that some data got loaded for this layer |      * Marks that some data got loaded for this layer | ||||||
|  | @ -16,14 +15,14 @@ export default class TileFreshnessCalculator { | ||||||
|     public addTileLoad(tileId: number, freshness: Date) { |     public addTileLoad(tileId: number, freshness: Date) { | ||||||
|         const existingFreshness = this.freshnessFor(...Tiles.tile_from_index(tileId)) |         const existingFreshness = this.freshnessFor(...Tiles.tile_from_index(tileId)) | ||||||
|         if (existingFreshness >= freshness) { |         if (existingFreshness >= freshness) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         this.freshnesses.set(tileId, freshness) |         this.freshnesses.set(tileId, freshness) | ||||||
| 
 | 
 | ||||||
|         // Do we have freshness for the neighbouring tiles? If so, we can mark the tile above as loaded too!
 |         // 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) |         let [z, x, y] = Tiles.tile_from_index(tileId) | ||||||
|         if (z === 0) { |         if (z === 0) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         x = x - (x % 2) // Make the tiles always even
 |         x = x - (x % 2) // Make the tiles always even
 | ||||||
|         y = y - (y % 2) |         y = y - (y % 2) | ||||||
|  | @ -48,11 +47,7 @@ export default class TileFreshnessCalculator { | ||||||
|         const leastFresh = Math.min(ul, ur, ll, lr) |         const leastFresh = Math.min(ul, ur, ll, lr) | ||||||
|         const date = new Date() |         const date = new Date() | ||||||
|         date.setTime(leastFresh) |         date.setTime(leastFresh) | ||||||
|         this.addTileLoad( |         this.addTileLoad(Tiles.tile_index(z - 1, Math.floor(x / 2), Math.floor(y / 2)), date) | ||||||
|             Tiles.tile_index(z - 1, Math.floor(x / 2), Math.floor(y / 2)), |  | ||||||
|             date |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public freshnessFor(z: number, x: number, y: number): Date { |     public freshnessFor(z: number, x: number, y: number): Date { | ||||||
|  | @ -65,7 +60,5 @@ export default class TileFreshnessCalculator { | ||||||
|         } |         } | ||||||
|         // recurse up
 |         // recurse up
 | ||||||
|         return this.freshnessFor(z - 1, Math.floor(x / 2), Math.floor(y / 2)) |         return this.freshnessFor(z - 1, Math.floor(x / 2), Math.floor(y / 2)) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,21 +1,22 @@ | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import { UIEventSource } from "../../UIEventSource" | ||||||
| import DynamicTileSource from "./DynamicTileSource"; | import DynamicTileSource from "./DynamicTileSource" | ||||||
| import {Utils} from "../../../Utils"; | import { Utils } from "../../../Utils" | ||||||
| import GeoJsonSource from "../Sources/GeoJsonSource"; | import GeoJsonSource from "../Sources/GeoJsonSource" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| 
 | 
 | ||||||
| export default class DynamicGeoJsonTileSource extends DynamicTileSource { | export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||||
| 
 |  | ||||||
|     private static whitelistCache = new Map<string, any>() |     private static whitelistCache = new Map<string, any>() | ||||||
| 
 | 
 | ||||||
|     constructor(layer: FilteredLayer, |     constructor( | ||||||
|                 registerLayer: (layer: FeatureSourceForLayer & Tiled) => void, |         layer: FilteredLayer, | ||||||
|                 state: { |         registerLayer: (layer: FeatureSourceForLayer & Tiled) => void, | ||||||
|                     locationControl?: UIEventSource<{zoom?: number}> |         state: { | ||||||
|                     currentBounds: UIEventSource<BBox> |             locationControl?: UIEventSource<{ zoom?: number }> | ||||||
|                 }) { |             currentBounds: UIEventSource<BBox> | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|         const source = layer.layerDef.source |         const source = layer.layerDef.source | ||||||
|         if (source.geojsonZoomLevel === undefined) { |         if (source.geojsonZoomLevel === undefined) { | ||||||
|             throw "Invalid layer: geojsonZoomLevel expected" |             throw "Invalid layer: geojsonZoomLevel expected" | ||||||
|  | @ -26,7 +27,6 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||||
| 
 | 
 | ||||||
|         let whitelist = undefined |         let whitelist = undefined | ||||||
|         if (source.geojsonSource.indexOf("{x}_{y}.geojson") > 0) { |         if (source.geojsonSource.indexOf("{x}_{y}.geojson") > 0) { | ||||||
| 
 |  | ||||||
|             const whitelistUrl = source.geojsonSource |             const whitelistUrl = source.geojsonSource | ||||||
|                 .replace("{z}", "" + source.geojsonZoomLevel) |                 .replace("{z}", "" + source.geojsonZoomLevel) | ||||||
|                 .replace("{x}_{y}.geojson", "overview.json") |                 .replace("{x}_{y}.geojson", "overview.json") | ||||||
|  | @ -35,26 +35,33 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||||
|             if (DynamicGeoJsonTileSource.whitelistCache.has(whitelistUrl)) { |             if (DynamicGeoJsonTileSource.whitelistCache.has(whitelistUrl)) { | ||||||
|                 whitelist = DynamicGeoJsonTileSource.whitelistCache.get(whitelistUrl) |                 whitelist = DynamicGeoJsonTileSource.whitelistCache.get(whitelistUrl) | ||||||
|             } else { |             } else { | ||||||
|                 Utils.downloadJsonCached(whitelistUrl, 1000 * 60 * 60).then( |                 Utils.downloadJsonCached(whitelistUrl, 1000 * 60 * 60) | ||||||
|                     json => { |                     .then((json) => { | ||||||
|                         const data = new Map<number, Set<number>>(); |                         const data = new Map<number, Set<number>>() | ||||||
|                         for (const x in json) { |                         for (const x in json) { | ||||||
|                             if (x === "zoom") { |                             if (x === "zoom") { | ||||||
|                                 continue |                                 continue | ||||||
|                             } |                             } | ||||||
|                             data.set(Number(x), new Set(json[x])) |                             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 |                         whitelist = data | ||||||
|                         DynamicGeoJsonTileSource.whitelistCache.set(whitelistUrl, whitelist) |                         DynamicGeoJsonTileSource.whitelistCache.set(whitelistUrl, whitelist) | ||||||
|                     } |                     }) | ||||||
|                 ).catch(err => { |                     .catch((err) => { | ||||||
|                     console.warn("No whitelist found for ", layer.layerDef.id, err) |                         console.warn("No whitelist found for ", layer.layerDef.id, err) | ||||||
|                 }) |                     }) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const blackList = (new Set<string>()) |         const blackList = new Set<string>() | ||||||
|         super( |         super( | ||||||
|             layer, |             layer, | ||||||
|             source.geojsonZoomLevel, |             source.geojsonZoomLevel, | ||||||
|  | @ -62,29 +69,28 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||||
|                 if (whitelist !== undefined) { |                 if (whitelist !== undefined) { | ||||||
|                     const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2]) |                     const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2]) | ||||||
|                     if (!isWhiteListed) { |                     if (!isWhiteListed) { | ||||||
|                         console.debug("Not downloading tile", ...zxy, "as it is not on the whitelist") |                         console.debug( | ||||||
|                         return undefined; |                             "Not downloading tile", | ||||||
|  |                             ...zxy, | ||||||
|  |                             "as it is not on the whitelist" | ||||||
|  |                         ) | ||||||
|  |                         return undefined | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 const src = new GeoJsonSource( |                 const src = new GeoJsonSource(layer, zxy, { | ||||||
|                     layer, |                     featureIdBlacklist: blackList, | ||||||
|                     zxy, |                 }) | ||||||
|                     { | 
 | ||||||
|                         featureIdBlacklist: blackList |  | ||||||
|                     } |  | ||||||
|                 ) |  | ||||||
|       |  | ||||||
|                 registerLayer(src) |                 registerLayer(src) | ||||||
|                 return src |                 return src | ||||||
|             }, |             }, | ||||||
|             state |             state | ||||||
|         ); |         ) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static RegisterWhitelist(url: string, json: any) { |     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) { |         for (const x in json) { | ||||||
|             if (x === "zoom") { |             if (x === "zoom") { | ||||||
|                 continue |                 continue | ||||||
|  | @ -93,5 +99,4 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||||
|         } |         } | ||||||
|         DynamicGeoJsonTileSource.whitelistCache.set(url, data) |         DynamicGeoJsonTileSource.whitelistCache.set(url, data) | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,64 +1,80 @@ | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import { UIEventSource } from "../../UIEventSource" | ||||||
| import TileHierarchy from "./TileHierarchy"; | import TileHierarchy from "./TileHierarchy" | ||||||
| import {Tiles} from "../../../Models/TileRange"; | import { Tiles } from "../../../Models/TileRange" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| 
 | 
 | ||||||
| /*** | /*** | ||||||
|  * A tiled source which dynamically loads the required tiles at a fixed zoom level |  * A tiled source which dynamically loads the required tiles at a fixed zoom level | ||||||
|  */ |  */ | ||||||
| export default class DynamicTileSource implements TileHierarchy<FeatureSourceForLayer & Tiled> { | export default class DynamicTileSource implements TileHierarchy<FeatureSourceForLayer & Tiled> { | ||||||
|     public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>; |     public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> | ||||||
|     private readonly _loadedTiles = new Set<number>(); |     private readonly _loadedTiles = new Set<number>() | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         layer: FilteredLayer, |         layer: FilteredLayer, | ||||||
|         zoomlevel: number, |         zoomlevel: number, | ||||||
|         constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled), |         constructTile: (zxy: [number, number, number]) => FeatureSourceForLayer & Tiled, | ||||||
|         state: { |         state: { | ||||||
|             currentBounds: UIEventSource<BBox>; |             currentBounds: UIEventSource<BBox> | ||||||
|             locationControl?: UIEventSource<{zoom?: number}> |             locationControl?: UIEventSource<{ zoom?: number }> | ||||||
|         } |         } | ||||||
|     ) { |     ) { | ||||||
|         const self = this; |         const self = this | ||||||
| 
 | 
 | ||||||
|         this.loadedTiles = new Map<number, FeatureSourceForLayer & Tiled>() |         this.loadedTiles = new Map<number, FeatureSourceForLayer & Tiled>() | ||||||
|         const neededTiles = state.currentBounds.map( |         const neededTiles = state.currentBounds | ||||||
|             bounds => { |             .map( | ||||||
|                 if (bounds === undefined) { |                 (bounds) => { | ||||||
|                     // We'll retry later
 |                     if (bounds === undefined) { | ||||||
|                     return undefined |                         // We'll retry later
 | ||||||
|                 } |                         return undefined | ||||||
|                  |                     } | ||||||
|                 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) { |                     if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) { | ||||||
|                     // No need to download! - the layer is disabled
 |                         // No need to download! - the layer is disabled
 | ||||||
|                     return undefined; |                         return undefined | ||||||
|                 } |                     } | ||||||
| 
 | 
 | ||||||
|                 const tileRange = Tiles.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) |                     if ( | ||||||
|                 if (tileRange.total > 10000) { |                         state.locationControl?.data?.zoom !== undefined && | ||||||
|                     console.error("Got a really big tilerange, bounds and location might be out of sync") |                         state.locationControl.data.zoom < layer.layerDef.minzoom | ||||||
|                     return undefined |                     ) { | ||||||
|                 } |                         // No need to download! - the layer is disabled
 | ||||||
|  |                         return undefined | ||||||
|  |                     } | ||||||
| 
 | 
 | ||||||
|                 const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i)) |                     const tileRange = Tiles.TileRangeBetween( | ||||||
|                 if (needed.length === 0) { |                         zoomlevel, | ||||||
|                     return undefined |                         bounds.getNorth(), | ||||||
|                 } |                         bounds.getEast(), | ||||||
|                 return needed |                         bounds.getSouth(), | ||||||
|             } |                         bounds.getWest() | ||||||
|             , [layer.isDisplayed, state.locationControl]).stabilized(250); |                     ) | ||||||
|  |                     if (tileRange.total > 10000) { | ||||||
|  |                         console.error( | ||||||
|  |                             "Got a really big tilerange, bounds and location might be out of sync" | ||||||
|  |                         ) | ||||||
|  |                         return undefined | ||||||
|  |                     } | ||||||
| 
 | 
 | ||||||
|         neededTiles.addCallbackAndRunD(neededIndexes => { |                     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) | ||||||
|  | 
 | ||||||
|  |         neededTiles.addCallbackAndRunD((neededIndexes) => { | ||||||
|             console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes) |             console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes) | ||||||
|             if (neededIndexes === undefined) { |             if (neededIndexes === undefined) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             for (const neededIndex of neededIndexes) { |             for (const neededIndex of neededIndexes) { | ||||||
|                 self._loadedTiles.add(neededIndex) |                 self._loadedTiles.add(neededIndex) | ||||||
|  | @ -68,10 +84,5 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -1,30 +1,26 @@ | ||||||
| import TileHierarchy from "./TileHierarchy"; | import TileHierarchy from "./TileHierarchy" | ||||||
| import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||||
| import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject"; | import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject" | ||||||
| import SimpleFeatureSource from "../Sources/SimpleFeatureSource"; | import SimpleFeatureSource from "../Sources/SimpleFeatureSource" | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import { UIEventSource } from "../../UIEventSource" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> { | export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> { | ||||||
|     public readonly loadedTiles = new Map<number, 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 layer: FilteredLayer | ||||||
|     private readonly nodeByIds = new Map<number, OsmNode>(); |     private readonly nodeByIds = new Map<number, OsmNode>() | ||||||
|     private readonly parentWays = new Map<number, UIEventSource<OsmWay[]>>() |     private readonly parentWays = new Map<number, UIEventSource<OsmWay[]>>() | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor(layer: FilteredLayer, onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void) { | ||||||
|         layer: FilteredLayer, |  | ||||||
|         onTileLoaded: ((tile: Tiled & FeatureSourceForLayer) => void)) { |  | ||||||
|         this.onTileLoaded = onTileLoaded |         this.onTileLoaded = onTileLoaded | ||||||
|         this.layer = layer; |         this.layer = layer | ||||||
|         if (this.layer === undefined) { |         if (this.layer === undefined) { | ||||||
|             throw "Layer is undefined" |             throw "Layer is undefined" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public handleOsmJson(osmJson: any, tileId: number) { |     public handleOsmJson(osmJson: any, tileId: number) { | ||||||
| 
 |  | ||||||
|         const allObjects = OsmObject.ParseObjects(osmJson.elements) |         const allObjects = OsmObject.ParseObjects(osmJson.elements) | ||||||
|         const nodesById = new Map<number, OsmNode>() |         const nodesById = new Map<number, OsmNode>() | ||||||
| 
 | 
 | ||||||
|  | @ -32,7 +28,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour | ||||||
|             if (osmObj.type !== "node") { |             if (osmObj.type !== "node") { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             const osmNode = <OsmNode>osmObj; |             const osmNode = <OsmNode>osmObj | ||||||
|             nodesById.set(osmNode.id, osmNode) |             nodesById.set(osmNode.id, osmNode) | ||||||
|             this.nodeByIds.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") { |             if (osmObj.type !== "way") { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             const osmWay = <OsmWay>osmObj; |             const osmWay = <OsmWay>osmObj | ||||||
|             for (const nodeId of osmWay.nodes) { |             for (const nodeId of osmWay.nodes) { | ||||||
| 
 |  | ||||||
|                 if (!this.parentWays.has(nodeId)) { |                 if (!this.parentWays.has(nodeId)) { | ||||||
|                     const src = new UIEventSource<OsmWay[]>([]) |                     const src = new UIEventSource<OsmWay[]>([]) | ||||||
|                     this.parentWays.set(nodeId, src) |                     this.parentWays.set(nodeId, src) | ||||||
|                     src.addCallback(parentWays => { |                     src.addCallback((parentWays) => { | ||||||
|                         const tgs = nodesById.get(nodeId).tags |                         const tgs = nodesById.get(nodeId).tags | ||||||
|                         tgs    ["parent_ways"] = JSON.stringify(parentWays.map(w => w.tags)) |                         tgs["parent_ways"] = JSON.stringify(parentWays.map((w) => w.tags)) | ||||||
|                         tgs["parent_way_ids"] = JSON.stringify(parentWays.map(w => w.id)) |                         tgs["parent_way_ids"] = JSON.stringify(parentWays.map((w) => w.id)) | ||||||
|                     }) |                     }) | ||||||
|                 } |                 } | ||||||
|                 const src = this.parentWays.get(nodeId) |                 const src = this.parentWays.get(nodeId) | ||||||
|                 src.data.push(osmWay) |                 src.data.push(osmWay) | ||||||
|                 src.ping(); |                 src.ping() | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         const now = new Date() |         const now = new Date() | ||||||
|         const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({ |         const asGeojsonFeatures = Array.from(nodesById.values()).map((osmNode) => ({ | ||||||
|             feature: osmNode.asGeoJson(), freshness: now |             feature: osmNode.asGeoJson(), | ||||||
|  |             freshness: now, | ||||||
|         })) |         })) | ||||||
| 
 | 
 | ||||||
|         const featureSource = new SimpleFeatureSource(this.layer, tileId) |         const featureSource = new SimpleFeatureSource(this.layer, tileId) | ||||||
|         featureSource.features.setData(asGeojsonFeatures) |         featureSource.features.setData(asGeojsonFeatures) | ||||||
|         this.loadedTiles.set(tileId, featureSource) |         this.loadedTiles.set(tileId, featureSource) | ||||||
|         this.onTileLoaded(featureSource) |         this.onTileLoaded(featureSource) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -88,6 +83,4 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour | ||||||
|     public GetParentWays(nodeId: number): UIEventSource<OsmWay[]> { |     public GetParentWays(nodeId: number): UIEventSource<OsmWay[]> { | ||||||
|         return this.parentWays.get(nodeId) |         return this.parentWays.get(nodeId) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -1,17 +1,17 @@ | ||||||
| import {Utils} from "../../../Utils"; | import { Utils } from "../../../Utils" | ||||||
| import * as OsmToGeoJson from "osmtogeojson"; | import * as OsmToGeoJson from "osmtogeojson" | ||||||
| import StaticFeatureSource from "../Sources/StaticFeatureSource"; | import StaticFeatureSource from "../Sources/StaticFeatureSource" | ||||||
| import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter"; | import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter" | ||||||
| import {Store, UIEventSource} from "../../UIEventSource"; | import { Store, UIEventSource } from "../../UIEventSource" | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||||
| import {Tiles} from "../../../Models/TileRange"; | import { Tiles } from "../../../Models/TileRange" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" | ||||||
| import {Or} from "../../Tags/Or"; | import { Or } from "../../Tags/Or" | ||||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | import { TagsFilter } from "../../Tags/TagsFilter" | ||||||
| import {OsmObject} from "../../Osm/OsmObject"; | import { OsmObject } from "../../Osm/OsmObject" | ||||||
| import {FeatureCollection} from "@turf/turf"; | 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' |  * 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 isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||||
|     public readonly downloadedTiles = new Set<number>() |     public readonly downloadedTiles = new Set<number>() | ||||||
|     public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = [] |     public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = [] | ||||||
|     private readonly _backend: string; |     private readonly _backend: string | ||||||
|     private readonly filteredLayers: Store<FilteredLayer[]>; |     private readonly filteredLayers: Store<FilteredLayer[]> | ||||||
|     private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void; |     private readonly handleTile: (fs: FeatureSourceForLayer & Tiled) => void | ||||||
|     private isActive: Store<boolean>; |     private isActive: Store<boolean> | ||||||
|     private options: { |     private options: { | ||||||
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void; |         handleTile: (tile: FeatureSourceForLayer & Tiled) => void | ||||||
|         isActive: Store<boolean>, |         isActive: Store<boolean> | ||||||
|         neededTiles: Store<number[]>, |         neededTiles: Store<number[]> | ||||||
|         markTileVisited?: (tileId: number) => void |         markTileVisited?: (tileId: number) => void | ||||||
|     }; |     } | ||||||
|     private readonly allowedTags: TagsFilter; |     private readonly allowedTags: TagsFilter | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * |      * | ||||||
|      * @param options: allowedFeatures is normally calculated from the layoutToUse |      * @param options: allowedFeatures is normally calculated from the layoutToUse | ||||||
|      */ |      */ | ||||||
|     constructor(options: { |     constructor(options: { | ||||||
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void; |         handleTile: (tile: FeatureSourceForLayer & Tiled) => void | ||||||
|         isActive: Store<boolean>, |         isActive: Store<boolean> | ||||||
|         neededTiles: Store<number[]>, |         neededTiles: Store<number[]> | ||||||
|         state: { |         state: { | ||||||
|             readonly filteredLayers: UIEventSource<FilteredLayer[]>; |             readonly filteredLayers: UIEventSource<FilteredLayer[]> | ||||||
|             readonly osmConnection: { |             readonly osmConnection: { | ||||||
|                 Backend(): string |                 Backend(): string | ||||||
|             }; |             } | ||||||
|             readonly layoutToUse?: LayoutConfig |             readonly layoutToUse?: LayoutConfig | ||||||
|         }, |         } | ||||||
|         readonly allowedFeatures?: TagsFilter, |         readonly allowedFeatures?: TagsFilter | ||||||
|         markTileVisited?: (tileId: number) => void |         markTileVisited?: (tileId: number) => void | ||||||
|     }) { |     }) { | ||||||
|         this.options = options; |         this.options = options | ||||||
|         this._backend = options.state.osmConnection.Backend(); |         this._backend = options.state.osmConnection.Backend() | ||||||
|         this.filteredLayers = options.state.filteredLayers.map(layers => layers.filter(layer => layer.layerDef.source.geojsonSource === undefined)) |         this.filteredLayers = options.state.filteredLayers.map((layers) => | ||||||
|  |             layers.filter((layer) => layer.layerDef.source.geojsonSource === undefined) | ||||||
|  |         ) | ||||||
|         this.handleTile = options.handleTile |         this.handleTile = options.handleTile | ||||||
|         this.isActive = options.isActive |         this.isActive = options.isActive | ||||||
|         const self = this |         const self = this | ||||||
|         options.neededTiles.addCallbackAndRunD(neededTiles => { |         options.neededTiles.addCallbackAndRunD((neededTiles) => { | ||||||
|             self.Update(neededTiles) |             self.Update(neededTiles) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         const neededLayers = (options.state.layoutToUse?.layers ?? []) |         const neededLayers = (options.state.layoutToUse?.layers ?? []) | ||||||
|             .filter(layer => !layer.doNotDownload) |             .filter((layer) => !layer.doNotDownload) | ||||||
|             .filter(layer => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer) |             .filter( | ||||||
|         this.allowedTags = options.allowedFeatures ?? new Or(neededLayers.map(l => l.source.osmTags)) |                 (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[]) { |     private async Update(neededTiles: number[]) { | ||||||
|         if (this.options.isActive?.data === false) { |         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) { |         if (neededTiles.length == 0) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.isRunning.setData(true) |         this.isRunning.setData(true) | ||||||
|         try { |         try { | ||||||
| 
 |  | ||||||
|             for (const neededTile of neededTiles) { |             for (const neededTile of neededTiles) { | ||||||
|                 this.downloadedTiles.add(neededTile) |                 this.downloadedTiles.add(neededTile) | ||||||
|                 await this.LoadTile(...Tiles.tile_from_index(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. |      * 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 |      * If the feature is already complete (or is not a relation), the feature will be returned | ||||||
|      */ |      */ | ||||||
|     private async patchIncompleteRelations(feature: {properties: {id: string}},  |     private async patchIncompleteRelations( | ||||||
|                                            originalJson: {elements: {type: "node" | "way" | "relation", id: number, } []}): Promise<any> { |         feature: { properties: { id: string } }, | ||||||
|         if(!feature.properties.id.startsWith("relation")){ |         originalJson: { elements: { type: "node" | "way" | "relation"; id: number }[] } | ||||||
|  |     ): Promise<any> { | ||||||
|  |         if (!feature.properties.id.startsWith("relation")) { | ||||||
|             return feature |             return feature | ||||||
|         } |         } | ||||||
|         const relationSpec = originalJson.elements.find(f => "relation/"+f.id === feature.properties.id) |         const relationSpec = originalJson.elements.find( | ||||||
|         const members : {type: string, ref: number}[] = relationSpec["members"] |             (f) => "relation/" + f.id === feature.properties.id | ||||||
|  |         ) | ||||||
|  |         const members: { type: string; ref: number }[] = relationSpec["members"] | ||||||
|         for (const member of 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) { |             if (isFound) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|              | 
 | ||||||
|             // This member is missing. We redownload the entire relation instead
 |             // 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 (await OsmObject.DownloadObjectAsync(feature.properties.id)).asGeoJson() | ||||||
|         } |         } | ||||||
|         return feature; |         return feature | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async LoadTile(z, x, y): Promise<void> { |     private async LoadTile(z, x, y): Promise<void> { | ||||||
|  | @ -130,52 +139,69 @@ export default class OsmFeatureSource { | ||||||
|         const bbox = BBox.fromTile(z, x, y) |         const bbox = BBox.fromTile(z, x, y) | ||||||
|         const url = `${this._backend}/api/0.6/map?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` |         const url = `${this._backend}/api/0.6/map?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` | ||||||
| 
 | 
 | ||||||
|         let error = undefined; |         let error = undefined | ||||||
|         try { |         try { | ||||||
|             const osmJson = await Utils.downloadJson(url) |             const osmJson = await Utils.downloadJson(url) | ||||||
|             try { |             try { | ||||||
| 
 |  | ||||||
|                 console.log("Got tile", z, x, y, "from the osm api") |                 console.log("Got tile", z, x, y, "from the osm api") | ||||||
|                 this.rawDataHandlers.forEach(handler => handler(osmJson, Tiles.tile_index(z, x, y))) |                 this.rawDataHandlers.forEach((handler) => | ||||||
|                 const geojson = <FeatureCollection<any , {id: string}>> OsmToGeoJson.default(osmJson, |                     handler(osmJson, Tiles.tile_index(z, x, y)) | ||||||
|  |                 ) | ||||||
|  |                 const geojson = <FeatureCollection<any, { id: string }>>OsmToGeoJson.default( | ||||||
|  |                     osmJson, | ||||||
|                     // @ts-ignore
 |                     // @ts-ignore
 | ||||||
|                     { |                     { | ||||||
|                         flatProperties: true |                         flatProperties: true, | ||||||
|                     }); |                     } | ||||||
| 
 |                 ) | ||||||
| 
 | 
 | ||||||
|                 // The geojson contains _all_ features at the given location
 |                 // The geojson contains _all_ features at the given location
 | ||||||
|                 // We only keep what is needed
 |                 // 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++) { |                 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 |                     f.properties["_backend"] = this._backend | ||||||
|                 }) |                 }) | ||||||
| 
 | 
 | ||||||
|                 const index = Tiles.tile_index(z, x, y); |                 const index = Tiles.tile_index(z, x, y) | ||||||
|                 new PerLayerFeatureSourceSplitter(this.filteredLayers, |                 new PerLayerFeatureSourceSplitter( | ||||||
|  |                     this.filteredLayers, | ||||||
|                     this.handleTile, |                     this.handleTile, | ||||||
|                     StaticFeatureSource.fromGeojson(geojson.features), |                     StaticFeatureSource.fromGeojson(geojson.features), | ||||||
|                     { |                     { | ||||||
|                         tileIndex: index |                         tileIndex: index, | ||||||
|                     } |                     } | ||||||
|                 ); |                 ) | ||||||
|                 if (this.options.markTileVisited) { |                 if (this.options.markTileVisited) { | ||||||
|                     this.options.markTileVisited(index) |                     this.options.markTileVisited(index) | ||||||
|                 } |                 } | ||||||
|             }catch(e){ |             } catch (e) { | ||||||
|                 console.error("PANIC: got the tile from the OSM-api, but something crashed handling this tile") |                 console.error( | ||||||
|                 error = e; |                     "PANIC: got the tile from the OSM-api, but something crashed handling this tile" | ||||||
|  |                 ) | ||||||
|  |                 error = e | ||||||
|             } |             } | ||||||
|              |  | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.error("Could not download tile", z, x, y, "due to", e, "; retrying with smaller bounds") |             console.error( | ||||||
|  |                 "Could not download tile", | ||||||
|  |                 z, | ||||||
|  |                 x, | ||||||
|  |                 y, | ||||||
|  |                 "due to", | ||||||
|  |                 e, | ||||||
|  |                 "; retrying with smaller bounds" | ||||||
|  |             ) | ||||||
|             if (e === "rate limited") { |             if (e === "rate limited") { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             await this.LoadTile(z + 1, x * 2, y * 2) |             await this.LoadTile(z + 1, x * 2, y * 2) | ||||||
|             await this.LoadTile(z + 1, 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) |             await this.LoadTile(z + 1, 1 + x * 2, 1 + y * 2) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if(error !== undefined){ |         if (error !== undefined) { | ||||||
|             throw error; |             throw error | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,25 +1,24 @@ | ||||||
| import FeatureSource, {Tiled} from "../FeatureSource"; | import FeatureSource, { Tiled } from "../FeatureSource" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| 
 | 
 | ||||||
| export default interface TileHierarchy<T extends FeatureSource & Tiled> { | export default interface TileHierarchy<T extends FeatureSource & Tiled> { | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * A mapping from 'tile_index' to the actual tile featrues |      * A mapping from 'tile_index' to the actual tile featrues | ||||||
|      */ |      */ | ||||||
|     loadedTiles: Map<number, T> |     loadedTiles: Map<number, T> | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class TileHierarchyTools { | export class TileHierarchyTools { | ||||||
| 
 |     public static getTiles<T extends FeatureSource & Tiled>( | ||||||
|     public static getTiles<T extends FeatureSource & Tiled>(hierarchy: TileHierarchy<T>, bbox: BBox): T[] { |         hierarchy: TileHierarchy<T>, | ||||||
|  |         bbox: BBox | ||||||
|  |     ): T[] { | ||||||
|         const result: T[] = [] |         const result: T[] = [] | ||||||
|         hierarchy.loadedTiles.forEach((tile) => { |         hierarchy.loadedTiles.forEach((tile) => { | ||||||
|             if (tile.bbox.overlapsWith(bbox)) { |             if (tile.bbox.overlapsWith(bbox)) { | ||||||
|                 result.push(tile) |                 result.push(tile) | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|         return result; |         return result | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,20 +1,32 @@ | ||||||
| import TileHierarchy from "./TileHierarchy"; | import TileHierarchy from "./TileHierarchy" | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import { UIEventSource } from "../../UIEventSource" | ||||||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; | import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import FeatureSourceMerger from "../Sources/FeatureSourceMerger"; | import FeatureSourceMerger from "../Sources/FeatureSourceMerger" | ||||||
| import {Tiles} from "../../../Models/TileRange"; | import { Tiles } from "../../../Models/TileRange" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| 
 | 
 | ||||||
| export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> { | export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> { | ||||||
|     public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>(); |     public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map< | ||||||
|     public readonly layer: FilteredLayer; |         number, | ||||||
|     private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<number, UIEventSource<FeatureSource[]>>(); |         FeatureSourceForLayer & Tiled | ||||||
|     private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void; |     >() | ||||||
|  |     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) { |     constructor( | ||||||
|         this.layer = layer; |         layer: FilteredLayer, | ||||||
|         this._handleTile = handleTile; |         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 |      * @param src | ||||||
|      */ |      */ | ||||||
|     public registerTile(src: FeatureSource & Tiled) { |     public registerTile(src: FeatureSource & Tiled) { | ||||||
| 
 |  | ||||||
|         const index = src.tileIndex |         const index = src.tileIndex | ||||||
|         if (this.sources.has(index)) { |         if (this.sources.has(index)) { | ||||||
|             const sources = this.sources.get(index) |             const sources = this.sources.get(index) | ||||||
|             sources.data.push(src) |             sources.data.push(src) | ||||||
|             sources.ping() |             sources.ping() | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // We have to setup
 |         // We have to setup
 | ||||||
|         const sources = new UIEventSource<FeatureSource[]>([src]) |         const sources = new UIEventSource<FeatureSource[]>([src]) | ||||||
|         this.sources.set(index, sources) |         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.loadedTiles.set(index, merger) | ||||||
|         this._handleTile(merger, index) |         this._handleTile(merger, index) | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,53 +1,65 @@ | ||||||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; | import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" | ||||||
| import {Store, UIEventSource} from "../../UIEventSource"; | import { Store, UIEventSource } from "../../UIEventSource" | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer" | ||||||
| import TileHierarchy from "./TileHierarchy"; | import TileHierarchy from "./TileHierarchy" | ||||||
| import {Tiles} from "../../../Models/TileRange"; | import { Tiles } from "../../../Models/TileRange" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Contains all features in a tiled fashion. |  * 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 |  * 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> { | export default class TiledFeatureSource | ||||||
|     public readonly z: number; |     implements | ||||||
|     public readonly x: number; |         Tiled, | ||||||
|     public readonly y: number; |         IndexedFeatureSource, | ||||||
|     public readonly parent: TiledFeatureSource; |         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 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. |     /* An index of all known tiles. allTiles[z][x][y].get('layerid') will yield the corresponding tile. | ||||||
|     * Only defined on the root element! |      * 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 maxFeatureCount: number | ||||||
|     public readonly name; |     public readonly name | ||||||
|     public readonly features: UIEventSource<{ feature: any, freshness: Date }[]> |     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> | ||||||
|     public readonly containedIds: Store<Set<string>> |     public readonly containedIds: Store<Set<string>> | ||||||
| 
 | 
 | ||||||
|     public readonly bbox: BBox; |     public readonly bbox: BBox | ||||||
|     public readonly tileIndex: number; |     public readonly tileIndex: number | ||||||
|     private upper_left: TiledFeatureSource |     private upper_left: TiledFeatureSource | ||||||
|     private upper_right: TiledFeatureSource |     private upper_right: TiledFeatureSource | ||||||
|     private lower_left: TiledFeatureSource |     private lower_left: TiledFeatureSource | ||||||
|     private lower_right: TiledFeatureSource |     private lower_right: TiledFeatureSource | ||||||
|     private readonly maxzoom: number; |     private readonly maxzoom: number | ||||||
|     private readonly options: TiledFeatureSourceOptions |     private readonly options: TiledFeatureSourceOptions | ||||||
| 
 | 
 | ||||||
|     private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) { |     private constructor( | ||||||
|         this.z = z; |         z: number, | ||||||
|         this.x = x; |         x: number, | ||||||
|         this.y = y; |         y: number, | ||||||
|  |         parent: TiledFeatureSource, | ||||||
|  |         options?: TiledFeatureSourceOptions | ||||||
|  |     ) { | ||||||
|  |         this.z = z | ||||||
|  |         this.x = x | ||||||
|  |         this.y = y | ||||||
|         this.bbox = BBox.fromTile(z, x, y) |         this.bbox = BBox.fromTile(z, x, y) | ||||||
|         this.tileIndex = Tiles.tile_index(z, x, y) |         this.tileIndex = Tiles.tile_index(z, x, y) | ||||||
|         this.name = `TiledFeatureSource(${z},${x},${y})` |         this.name = `TiledFeatureSource(${z},${x},${y})` | ||||||
|         this.parent = parent; |         this.parent = parent | ||||||
|         this.layer = options.layer |         this.layer = options.layer | ||||||
|         options = options ?? {} |         options = options ?? {} | ||||||
|         this.maxFeatureCount = options?.maxFeatureCount ?? 250; |         this.maxFeatureCount = options?.maxFeatureCount ?? 250 | ||||||
|         this.maxzoom = options.maxZoomLevel ?? 18 |         this.maxzoom = options.maxZoomLevel ?? 18 | ||||||
|         this.options = options; |         this.options = options | ||||||
|         if (parent === undefined) { |         if (parent === undefined) { | ||||||
|             throw "Parent is not allowed to be undefined. Use null instead" |             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" |             throw "Invalid root tile: z, x and y should all be null" | ||||||
|         } |         } | ||||||
|         if (parent === null) { |         if (parent === null) { | ||||||
|             this.root = this; |             this.root = this | ||||||
|             this.loadedTiles = new Map() |             this.loadedTiles = new Map() | ||||||
|         } else { |         } else { | ||||||
|             this.root = this.parent.root; |             this.root = this.parent.root | ||||||
|             this.loadedTiles = this.root.loadedTiles; |             this.loadedTiles = this.root.loadedTiles | ||||||
|             const i = Tiles.tile_index(z, x, y) |             const i = Tiles.tile_index(z, x, y) | ||||||
|             this.root.loadedTiles.set(i, this) |             this.root.loadedTiles.set(i, this) | ||||||
|         } |         } | ||||||
|         this.features = new UIEventSource<any[]>([]) |         this.features = new UIEventSource<any[]>([]) | ||||||
|         this.containedIds = this.features.map(features => { |         this.containedIds = this.features.map((features) => { | ||||||
|             if (features === undefined) { |             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
 |         // We register this tile, but only when there is some data in it
 | ||||||
|         if (this.options.registerTile !== undefined) { |         if (this.options.registerTile !== undefined) { | ||||||
|             this.features.addCallbackAndRunD(features => { |             this.features.addCallbackAndRunD((features) => { | ||||||
|                 if (features.length === 0) { |                 if (features.length === 0) { | ||||||
|                     return; |                     return | ||||||
|                 } |                 } | ||||||
|                 this.options.registerTile(this) |                 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 = { | ||||||
|             ...options, |             ...options, | ||||||
|             layer: features["layer"] ?? options.layer |             layer: features["layer"] ?? options.layer, | ||||||
|         } |         } | ||||||
|         const root = new TiledFeatureSource(0, 0, 0, null, options) |         const root = new TiledFeatureSource(0, 0, 0, null, options) | ||||||
|         features.features?.addCallbackAndRunD(feats => root.addFeatures(feats)) |         features.features?.addCallbackAndRunD((feats) => root.addFeatures(feats)) | ||||||
|         return root; |         return root | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private isSplitNeeded(featureCount: number) { |     private isSplitNeeded(featureCount: number) { | ||||||
|         if (this.upper_left !== undefined) { |         if (this.upper_left !== undefined) { | ||||||
|             // This tile has been split previously, so we keep on splitting
 |             // This tile has been split previously, so we keep on splitting
 | ||||||
|             return true; |             return true | ||||||
|         } |         } | ||||||
|         if (this.z >= this.maxzoom) { |         if (this.z >= this.maxzoom) { | ||||||
|             // We are not allowed to split any further
 |             // We are not allowed to split any further
 | ||||||
|  | @ -111,7 +124,6 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | ||||||
| 
 | 
 | ||||||
|         // To much features - we split
 |         // To much features - we split
 | ||||||
|         return featureCount > this.maxFeatureCount |         return featureCount > this.maxFeatureCount | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /*** |     /*** | ||||||
|  | @ -120,21 +132,45 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | ||||||
|      * @param features |      * @param features | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private addFeatures(features: { feature: any, freshness: Date }[]) { |     private addFeatures(features: { feature: any; freshness: Date }[]) { | ||||||
|         if (features === undefined || features.length === 0) { |         if (features === undefined || features.length === 0) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!this.isSplitNeeded(features.length)) { |         if (!this.isSplitNeeded(features.length)) { | ||||||
|             this.features.setData(features) |             this.features.setData(features) | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (this.upper_left === undefined) { |         if (this.upper_left === undefined) { | ||||||
|             this.upper_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2, this, this.options) |             this.upper_left = new TiledFeatureSource( | ||||||
|             this.upper_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2, this, this.options) |                 this.z + 1, | ||||||
|             this.lower_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2 + 1, this, this.options) |                 this.x * 2, | ||||||
|             this.lower_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2 + 1, this, this.options) |                 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 = [] |         const ulf = [] | ||||||
|  | @ -147,7 +183,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | ||||||
|             const bbox = BBox.get(feature.feature) |             const bbox = BBox.get(feature.feature) | ||||||
| 
 | 
 | ||||||
|             // There are a few strategies to deal with features that cross tile boundaries
 |             // There are a few strategies to deal with features that cross tile boundaries
 | ||||||
|              | 
 | ||||||
|             if (this.options.noDuplicates) { |             if (this.options.noDuplicates) { | ||||||
|                 // Strategy 1: We put the feature into a somewhat matching tile
 |                 // Strategy 1: We put the feature into a somewhat matching tile
 | ||||||
|                 if (bbox.overlapsWith(this.upper_left.bbox)) { |                 if (bbox.overlapsWith(this.upper_left.bbox)) { | ||||||
|  | @ -195,19 +231,18 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | ||||||
|         this.lower_left.addFeatures(llf) |         this.lower_left.addFeatures(llf) | ||||||
|         this.lower_right.addFeatures(lrf) |         this.lower_right.addFeatures(lrf) | ||||||
|         this.features.setData(overlapsboundary) |         this.features.setData(overlapsboundary) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface TiledFeatureSourceOptions { | export interface TiledFeatureSourceOptions { | ||||||
|     readonly maxFeatureCount?: number, |     readonly maxFeatureCount?: number | ||||||
|     readonly maxZoomLevel?: number, |     readonly maxZoomLevel?: number | ||||||
|     readonly minZoomLevel?: number, |     readonly minZoomLevel?: number | ||||||
|     /** |     /** | ||||||
|      * IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated. |      * 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. |      * Setting 'dontEnforceMinZoomLevel' will assign to feature to some matching subtile. | ||||||
|      */ |      */ | ||||||
|     readonly noDuplicates?: boolean, |     readonly noDuplicates?: boolean | ||||||
|     readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void, |     readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void | ||||||
|     readonly layer?: FilteredLayer |     readonly layer?: FilteredLayer | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,17 +1,25 @@ | ||||||
| import * as turf from '@turf/turf' | import * as turf from "@turf/turf" | ||||||
| import {BBox} from "./BBox"; | import { BBox } from "./BBox" | ||||||
| import togpx from "togpx" | import togpx from "togpx" | ||||||
| import Constants from "../Models/Constants"; | import Constants from "../Models/Constants" | ||||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||||
| import {AllGeoJSON, booleanWithin, Coord, Feature, Geometry, MultiPolygon, Polygon, Properties} from "@turf/turf"; | import { | ||||||
|  |     AllGeoJSON, | ||||||
|  |     booleanWithin, | ||||||
|  |     Coord, | ||||||
|  |     Feature, | ||||||
|  |     Geometry, | ||||||
|  |     MultiPolygon, | ||||||
|  |     Polygon, | ||||||
|  |     Properties, | ||||||
|  | } from "@turf/turf" | ||||||
| 
 | 
 | ||||||
| export class GeoOperations { | export class GeoOperations { | ||||||
| 
 |     private static readonly _earthRadius = 6378137 | ||||||
|     private static readonly _earthRadius = 6378137; |     private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2 | ||||||
|     private static readonly _originShift = 2 * Math.PI * GeoOperations._earthRadius / 2; |  | ||||||
| 
 | 
 | ||||||
|     static surfaceAreaInSqMeters(feature: any) { |     static surfaceAreaInSqMeters(feature: any) { | ||||||
|         return turf.area(feature); |         return turf.area(feature) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -19,10 +27,10 @@ export class GeoOperations { | ||||||
|      * @param feature |      * @param feature | ||||||
|      */ |      */ | ||||||
|     static centerpoint(feature: any) { |     static centerpoint(feature: any) { | ||||||
|         const newFeature = turf.center(feature); |         const newFeature = turf.center(feature) | ||||||
|         newFeature.properties = feature.properties; |         newFeature.properties = feature.properties | ||||||
|         newFeature.id = feature.id; |         newFeature.id = feature.id | ||||||
|         return newFeature; |         return newFeature | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -30,7 +38,7 @@ export class GeoOperations { | ||||||
|      * @param feature |      * @param feature | ||||||
|      */ |      */ | ||||||
|     static centerpointCoordinates(feature: AllGeoJSON): [number, number] { |     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 |      * @param lonlat1 | ||||||
|      */ |      */ | ||||||
|     static distanceBetween(lonlat0: [number, number], lonlat1: [number, number]) { |     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 }) { |     static convexHull(featureCollection, options: { concavity?: number }) { | ||||||
|  | @ -69,16 +77,17 @@ export class GeoOperations { | ||||||
|      * const overlap0 = GeoOperations.calculateOverlap(line0, [polygon]); |      * const overlap0 = GeoOperations.calculateOverlap(line0, [polygon]); | ||||||
|      * overlap.length // => 1
 |      * overlap.length // => 1
 | ||||||
|      */ |      */ | ||||||
|     static calculateOverlap(feature: any, otherFeatures: any[]): { feat: any, overlap: number }[] { |     static calculateOverlap(feature: any, otherFeatures: any[]): { feat: any; overlap: number }[] { | ||||||
| 
 |         const featureBBox = BBox.get(feature) | ||||||
|         const featureBBox = BBox.get(feature); |         const result: { feat: any; overlap: number }[] = [] | ||||||
|         const result: { feat: any, overlap: number }[] = []; |  | ||||||
|         if (feature.geometry.type === "Point") { |         if (feature.geometry.type === "Point") { | ||||||
|             const coor = feature.geometry.coordinates; |             const coor = feature.geometry.coordinates | ||||||
|             for (const otherFeature of otherFeatures) { |             for (const otherFeature of otherFeatures) { | ||||||
| 
 |                 if ( | ||||||
|                 if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) { |                     feature.properties.id !== undefined && | ||||||
|                     continue; |                     feature.properties.id === otherFeature.properties.id | ||||||
|  |                 ) { | ||||||
|  |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (otherFeature.geometry === undefined) { |                 if (otherFeature.geometry === undefined) { | ||||||
|  | @ -87,86 +96,105 @@ export class GeoOperations { | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (GeoOperations.inside(coor, otherFeature)) { |                 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") { |         if (feature.geometry.type === "LineString") { | ||||||
| 
 |  | ||||||
|             for (const otherFeature of otherFeatures) { |             for (const otherFeature of otherFeatures) { | ||||||
| 
 |                 if ( | ||||||
|                 if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) { |                     feature.properties.id !== undefined && | ||||||
|                     continue; |                     feature.properties.id === otherFeature.properties.id | ||||||
|  |                 ) { | ||||||
|  |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 const intersection = GeoOperations.calculateInstersection(feature, otherFeature, featureBBox) |                 const intersection = GeoOperations.calculateInstersection( | ||||||
|  |                     feature, | ||||||
|  |                     otherFeature, | ||||||
|  |                     featureBBox | ||||||
|  |                 ) | ||||||
|                 if (intersection === null) { |                 if (intersection === null) { | ||||||
|                     continue |                     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") { |         if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") { | ||||||
| 
 |  | ||||||
|             for (const otherFeature of otherFeatures) { |             for (const otherFeature of otherFeatures) { | ||||||
| 
 |                 if ( | ||||||
|                 if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) { |                     feature.properties.id !== undefined && | ||||||
|                     continue; |                     feature.properties.id === otherFeature.properties.id | ||||||
|  |                 ) { | ||||||
|  |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (otherFeature.geometry.type === "Point") { |                 if (otherFeature.geometry.type === "Point") { | ||||||
|                     if (this.inside(otherFeature, feature)) { |                     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
 |                 // Calculate the surface area of the intersection
 | ||||||
| 
 | 
 | ||||||
|                 const intersection = this.calculateInstersection(feature, otherFeature, featureBBox) |                 const intersection = this.calculateInstersection(feature, otherFeature, featureBBox) | ||||||
|                 if (intersection === null) { |                 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") |         console.error( | ||||||
|         return result; |             "Could not correctly calculate the overlap of ", | ||||||
|  |             feature, | ||||||
|  |             ": unsupported type" | ||||||
|  |         ) | ||||||
|  |         return result | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Helper function which does the heavy lifting for 'inside' |      * Helper function which does the heavy lifting for 'inside' | ||||||
|      */ |      */ | ||||||
|     private static pointInPolygonCoordinates(x: number, y: number, coordinates: [number, number][][]) { |     private static pointInPolygonCoordinates( | ||||||
|         const inside = GeoOperations.pointWithinRing(x, y, /*This is the outer ring of the polygon */coordinates[0]) |         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) { |         if (!inside) { | ||||||
|             return false; |             return false | ||||||
|         } |         } | ||||||
|         for (let i = 1; i < coordinates.length; i++) { |         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) { |             if (inHole) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return true; |         return true | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Detect wether or not the given point is located in the feature |      * Detect wether or not the given point is located in the feature | ||||||
|      *  |      * | ||||||
|      * // Should work with a normal polygon
 |      * // Should work with a normal polygon
 | ||||||
|      * const polygon = {"type": "Feature","properties": {},"geometry": {"type": "Polygon","coordinates": [[[1.8017578124999998,50.401515322782366],[-3.1640625,46.255846818480315],[5.185546875,44.74673324024678],[1.8017578124999998,50.401515322782366]]]}}; |      * const polygon = {"type": "Feature","properties": {},"geometry": {"type": "Polygon","coordinates": [[[1.8017578124999998,50.401515322782366],[-3.1640625,46.255846818480315],[5.185546875,44.74673324024678],[1.8017578124999998,50.401515322782366]]]}}; | ||||||
|      * GeoOperations.inside([3.779296875, 48.777912755501845], polygon) // => false
 |      * GeoOperations.inside([3.779296875, 48.777912755501845], polygon) // => false
 | ||||||
|      * GeoOperations.inside([1.23046875, 47.60616304386874], polygon) // => true
 |      * GeoOperations.inside([1.23046875, 47.60616304386874], polygon) // => true
 | ||||||
|      *  |      * | ||||||
|      * // should work with a multipolygon and detect holes
 |      * // should work with a multipolygon and detect holes
 | ||||||
|      * const multiPolygon = {"type": "Feature", "properties": {}, |      * const multiPolygon = {"type": "Feature", "properties": {}, | ||||||
|      *         "geometry": { |      *         "geometry": { | ||||||
|  | @ -186,37 +214,32 @@ export class GeoOperations { | ||||||
|         // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
 |         // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
 | ||||||
| 
 | 
 | ||||||
|         if (feature.geometry.type === "Point") { |         if (feature.geometry.type === "Point") { | ||||||
|             return false; |             return false | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (pointCoordinate.geometry !== undefined) { |         if (pointCoordinate.geometry !== undefined) { | ||||||
|             pointCoordinate = pointCoordinate.geometry.coordinates |             pointCoordinate = pointCoordinate.geometry.coordinates | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const x: number = pointCoordinate[0]; |         const x: number = pointCoordinate[0] | ||||||
|         const y: number = pointCoordinate[1]; |         const y: number = pointCoordinate[1] | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|         if (feature.geometry.type === "MultiPolygon") { |         if (feature.geometry.type === "MultiPolygon") { | ||||||
|             const coordinatess = feature.geometry.coordinates; |             const coordinatess = feature.geometry.coordinates | ||||||
|             for (const coordinates of coordinatess) { |             for (const coordinates of coordinatess) { | ||||||
|                 const inThisPolygon = GeoOperations.pointInPolygonCoordinates(x, y, coordinates) |                 const inThisPolygon = GeoOperations.pointInPolygonCoordinates(x, y, coordinates) | ||||||
|                 if (inThisPolygon) { |                 if (inThisPolygon) { | ||||||
|                     return true; |                     return true | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|             return false; |             return false | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         if (feature.geometry.type === "Polygon") { |         if (feature.geometry.type === "Polygon") { | ||||||
|             return GeoOperations.pointInPolygonCoordinates(x, y, feature.geometry.coordinates) |             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) { |     static lengthInMeters(feature: any) { | ||||||
|  | @ -225,39 +248,24 @@ export class GeoOperations { | ||||||
| 
 | 
 | ||||||
|     static buffer(feature: any, bufferSizeInMeter: number) { |     static buffer(feature: any, bufferSizeInMeter: number) { | ||||||
|         return turf.buffer(feature, bufferSizeInMeter / 1000, { |         return turf.buffer(feature, bufferSizeInMeter / 1000, { | ||||||
|             units: 'kilometers' |             units: "kilometers", | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static bbox(feature: any) { |     static bbox(feature: any) { | ||||||
|         const [lon, lat, lon0, lat0] = turf.bbox(feature) |         const [lon, lat, lon0, lat0] = turf.bbox(feature) | ||||||
|         return { |         return { | ||||||
|             "type": "Feature", |             type: "Feature", | ||||||
|             "geometry": { |             geometry: { | ||||||
|                 "type": "LineString", |                 type: "LineString", | ||||||
|                 "coordinates": [ |                 coordinates: [ | ||||||
|                     [ |                     [lon, lat], | ||||||
|                         lon, |                     [lon0, lat], | ||||||
|                         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]) { |     public static nearestPoint(way, point: [number, number]) { | ||||||
|         if (way.geometry.type === "Polygon") { |         if (way.geometry.type === "Polygon") { | ||||||
|             way = {...way} |             way = { ...way } | ||||||
|             way.geometry = {...way.geometry} |             way.geometry = { ...way.geometry } | ||||||
|             way.geometry.type = "LineString" |             way.geometry.type = "LineString" | ||||||
|             way.geometry.coordinates = way.geometry.coordinates[0] |             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 { |     public static toCSV(features: any[]): string { | ||||||
| 
 |         const headerValuesSeen = new Set<string>() | ||||||
|         const headerValuesSeen = new Set<string>(); |  | ||||||
|         const headerValuesOrdered: string[] = [] |         const headerValuesOrdered: string[] = [] | ||||||
| 
 | 
 | ||||||
|         function addH(key) { |         function addH(key) { | ||||||
|  | @ -300,18 +307,17 @@ export class GeoOperations { | ||||||
|         const lines: string[] = [] |         const lines: string[] = [] | ||||||
| 
 | 
 | ||||||
|         for (const feature of features) { |         for (const feature of features) { | ||||||
|             const properties = feature.properties; |             const properties = feature.properties | ||||||
|             for (const key in properties) { |             for (const key in properties) { | ||||||
|                 if (!properties.hasOwnProperty(key)) { |                 if (!properties.hasOwnProperty(key)) { | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
|                 addH(key) |                 addH(key) | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         headerValuesOrdered.sort() |         headerValuesOrdered.sort() | ||||||
|         for (const feature of features) { |         for (const feature of features) { | ||||||
|             const properties = feature.properties; |             const properties = feature.properties | ||||||
|             let line = "" |             let line = "" | ||||||
|             for (const key of headerValuesOrdered) { |             for (const key of headerValuesOrdered) { | ||||||
|                 const value = properties[key] |                 const value = properties[key] | ||||||
|  | @ -324,27 +330,27 @@ export class GeoOperations { | ||||||
|             lines.push(line) |             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
 |     //Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913
 | ||||||
|     public static ConvertWgs84To900913(lonLat: [number, number]): [number, number] { |     public static ConvertWgs84To900913(lonLat: [number, number]): [number, number] { | ||||||
|         const lon = lonLat[0]; |         const lon = lonLat[0] | ||||||
|         const lat = lonLat[1]; |         const lat = lonLat[1] | ||||||
|         const x = lon * GeoOperations._originShift / 180; |         const x = (lon * GeoOperations._originShift) / 180 | ||||||
|         let y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); |         let y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360)) / (Math.PI / 180) | ||||||
|         y = y * GeoOperations._originShift / 180; |         y = (y * GeoOperations._originShift) / 180 | ||||||
|         return [x, y]; |         return [x, y] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     //Converts XY point from (Spherical) Web Mercator EPSG:3785 (unofficially EPSG:900913) to lat/lon in WGS84 Datum
 |     //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] { |     public static Convert900913ToWgs84(lonLat: [number, number]): [number, number] { | ||||||
|         const lon = lonLat[0] |         const lon = lonLat[0] | ||||||
|         const lat = lonLat[1] |         const lat = lonLat[1] | ||||||
|         const x = 180 * lon / GeoOperations._originShift; |         const x = (180 * lon) / GeoOperations._originShift | ||||||
|         let y = 180 * lat / GeoOperations._originShift; |         let y = (180 * lat) / GeoOperations._originShift | ||||||
|         y = 180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2); |         y = (180 / Math.PI) * (2 * Math.atan(Math.exp((y * Math.PI) / 180)) - Math.PI / 2) | ||||||
|         return [x, y]; |         return [x, y] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static GeoJsonToWGS84(geojson) { |     public static GeoJsonToWGS84(geojson) { | ||||||
|  | @ -360,10 +366,10 @@ export class GeoOperations { | ||||||
|     public static SimplifyCoordinates(coordinates: [number, number][]) { |     public static SimplifyCoordinates(coordinates: [number, number][]) { | ||||||
|         const newCoordinates = [] |         const newCoordinates = [] | ||||||
|         for (let i = 1; i < coordinates.length - 1; i++) { |         for (let i = 1; i < coordinates.length - 1; i++) { | ||||||
|             const coordinate = coordinates[i]; |             const coordinate = coordinates[i] | ||||||
|             const prev = coordinates[i - 1] |             const prev = coordinates[i - 1] | ||||||
|             const next = 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 b1 = turf.bearing(coordinate, next) | ||||||
| 
 | 
 | ||||||
|             const diff = Math.abs(b1 - b0) |             const diff = Math.abs(b1 - b0) | ||||||
|  | @ -373,27 +379,27 @@ export class GeoOperations { | ||||||
|             newCoordinates.push(coordinate) |             newCoordinates.push(coordinate) | ||||||
|         } |         } | ||||||
|         return newCoordinates |         return newCoordinates | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Calculates line intersection between two features. |      * Calculates line intersection between two features. | ||||||
|      */ |      */ | ||||||
|     public static LineIntersections(feature, otherFeature): [number, number][] { |     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) { |     public static AsGpx(feature, generatedWithLayer?: LayerConfig) { | ||||||
| 
 |  | ||||||
|         const metadata = {} |         const metadata = {} | ||||||
|         const tags = feature.properties |         const tags = feature.properties | ||||||
| 
 | 
 | ||||||
|         if (generatedWithLayer !== undefined) { |         if (generatedWithLayer !== undefined) { | ||||||
| 
 |  | ||||||
|             metadata["name"] = generatedWithLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt |             metadata["name"] = generatedWithLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt | ||||||
|             metadata["desc"] = "Generated with MapComplete layer " + generatedWithLayer.id |             metadata["desc"] = "Generated with MapComplete layer " + generatedWithLayer.id | ||||||
|             if (tags._backend?.contains("openstreetmap")) { |             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["author"] = tags["_last_edit:contributor"] | ||||||
|                 metadata["link"] = "https://www.openstreetmap.org/" + tags.id |                 metadata["link"] = "https://www.openstreetmap.org/" + tags.id | ||||||
|                 metadata["time"] = tags["_last_edit:timestamp"] |                 metadata["time"] = tags["_last_edit:timestamp"] | ||||||
|  | @ -404,18 +410,22 @@ export class GeoOperations { | ||||||
| 
 | 
 | ||||||
|         return togpx(feature, { |         return togpx(feature, { | ||||||
|             creator: "MapComplete " + Constants.vNumber, |             creator: "MapComplete " + Constants.vNumber, | ||||||
|             metadata |             metadata, | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static IdentifieCommonSegments(coordinatess: [number, number][][]): { |     public static IdentifieCommonSegments(coordinatess: [number, number][][]): { | ||||||
|         originalIndex: number, |         originalIndex: number | ||||||
|         segmentShardWith: number[], |         segmentShardWith: number[] | ||||||
|         coordinates: [] |         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])
 |         // 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:
 |         // The strategy:
 | ||||||
|         // 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them
 |         // 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>() |         const allEdgesByKey = new Map<string, edge>() | ||||||
| 
 | 
 | ||||||
|         for (let index = 0; index < coordinatess.length; index++) { |         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++) { |             for (let i = 0; i < coordinates.length - 1; i++) { | ||||||
| 
 |                 const c0 = coordinates[i] | ||||||
|                 const c0 = coordinates[i]; |  | ||||||
|                 const c1 = coordinates[i + 1] |                 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 |                 let key: string | ||||||
|                 if (isReversed) { |                 if (isReversed) { | ||||||
|  | @ -438,40 +447,38 @@ export class GeoOperations { | ||||||
|                 } else { |                 } else { | ||||||
|                     key = "" + c0 + ";" + c1 |                     key = "" + c0 + ";" + c1 | ||||||
|                 } |                 } | ||||||
|                 const member = {index, isReversed} |                 const member = { index, isReversed } | ||||||
|                 if (allEdgesByKey.has(key)) { |                 if (allEdgesByKey.has(key)) { | ||||||
|                     allEdgesByKey.get(key).members.push(member) |                     allEdgesByKey.get(key).members.push(member) | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 let edge: edge; |                 let edge: edge | ||||||
|                 if (!isReversed) { |                 if (!isReversed) { | ||||||
|                     edge = { |                     edge = { | ||||||
|                         start: c0, |                         start: c0, | ||||||
|                         end: c1, |                         end: c1, | ||||||
|                         members: [member], |                         members: [member], | ||||||
|                         intermediate: [] |                         intermediate: [], | ||||||
|                     } |                     } | ||||||
|                 } else { |                 } else { | ||||||
|                     edge = { |                     edge = { | ||||||
|                         start: c1, |                         start: c1, | ||||||
|                         end: c0, |                         end: c0, | ||||||
|                         members: [member], |                         members: [member], | ||||||
|                         intermediate: [] |                         intermediate: [], | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 allEdgesByKey.set(key, edge) |                 allEdgesByKey.set(key, edge) | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Lets merge them back together!
 |         // Lets merge them back together!
 | ||||||
| 
 | 
 | ||||||
|         let didMergeSomething = false; |         let didMergeSomething = false | ||||||
|         let allMergedEdges = Array.from(allEdgesByKey.values()) |         let allMergedEdges = Array.from(allEdgesByKey.values()) | ||||||
|         const allEdgesByStartPoint = new Map<string, edge[]>() |         const allEdgesByStartPoint = new Map<string, edge[]>() | ||||||
|         for (const edge of allMergedEdges) { |         for (const edge of allMergedEdges) { | ||||||
| 
 |  | ||||||
|             edge.members.sort((m0, m1) => m0.index - m1.index) |             edge.members.sort((m0, m1) => m0.index - m1.index) | ||||||
| 
 | 
 | ||||||
|             const kstart = edge.start + "" |             const kstart = edge.start + "" | ||||||
|  | @ -481,7 +488,6 @@ export class GeoOperations { | ||||||
|             allEdgesByStartPoint.get(kstart).push(edge) |             allEdgesByStartPoint.get(kstart).push(edge) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         function membersAreCompatible(first: edge, second: edge): boolean { |         function membersAreCompatible(first: edge, second: edge): boolean { | ||||||
|             // There must be an exact match between the members
 |             // There must be an exact match between the members
 | ||||||
|             if (first.members === second.members) { |             if (first.members === second.members) { | ||||||
|  | @ -504,7 +510,6 @@ export class GeoOperations { | ||||||
|             // Allrigth, they are the same, lets mark this permanently
 |             // Allrigth, they are the same, lets mark this permanently
 | ||||||
|             second.members = first.members |             second.members = first.members | ||||||
|             return true |             return true | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         do { |         do { | ||||||
|  | @ -524,9 +529,8 @@ export class GeoOperations { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|                 for (let i = 0; i < matchingEndEdges.length; i++) { |                 for (let i = 0; i < matchingEndEdges.length; i++) { | ||||||
|                     const endEdge = matchingEndEdges[i]; |                     const endEdge = matchingEndEdges[i] | ||||||
| 
 | 
 | ||||||
|                     if (consumed.has(endEdge)) { |                     if (consumed.has(endEdge)) { | ||||||
|                         continue |                         continue | ||||||
|  | @ -543,12 +547,11 @@ export class GeoOperations { | ||||||
|                     edge.end = endEdge.end |                     edge.end = endEdge.end | ||||||
|                     consumed.add(endEdge) |                     consumed.add(endEdge) | ||||||
|                     matchingEndEdges.splice(i, 1) |                     matchingEndEdges.splice(i, 1) | ||||||
|                     break; |                     break | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             allMergedEdges = allMergedEdges.filter(edge => !consumed.has(edge)); |             allMergedEdges = allMergedEdges.filter((edge) => !consumed.has(edge)) | ||||||
| 
 |  | ||||||
|         } while (didMergeSomething) |         } while (didMergeSomething) | ||||||
| 
 | 
 | ||||||
|         return [] |         return [] | ||||||
|  | @ -557,7 +560,7 @@ export class GeoOperations { | ||||||
|     /** |     /** | ||||||
|      * Removes points that do not contribute to the geometry from linestrings and the outer ring of polygons. |      * Removes points that do not contribute to the geometry from linestrings and the outer ring of polygons. | ||||||
|      * Returs a new copy of the feature |      * Returs a new copy of the feature | ||||||
|      *  |      * | ||||||
|      * const feature = {"geometry": {"type": "Polygon","coordinates": [[[4.477944199999975,51.02783550000022],[4.477987899999996,51.027818800000034],[4.478004500000021,51.02783399999988],[4.478025499999962,51.02782489999994],[4.478079099999993,51.027873899999896],[4.47801040000006,51.027903799999955],[4.477964799999972,51.02785709999982],[4.477964699999964,51.02785690000006],[4.477944199999975,51.02783550000022]]]}} |      * const feature = {"geometry": {"type": "Polygon","coordinates": [[[4.477944199999975,51.02783550000022],[4.477987899999996,51.027818800000034],[4.478004500000021,51.02783399999988],[4.478025499999962,51.02782489999994],[4.478079099999993,51.027873899999896],[4.47801040000006,51.027903799999955],[4.477964799999972,51.02785709999982],[4.477964699999964,51.02785690000006],[4.477944199999975,51.02783550000022]]]}} | ||||||
|      * const copy = GeoOperations.removeOvernoding(feature) |      * const copy = GeoOperations.removeOvernoding(feature) | ||||||
|      * expect(copy.geometry.coordinates[0]).deep.equal([[4.477944199999975,51.02783550000022],[4.477987899999996,51.027818800000034],[4.478004500000021,51.02783399999988],[4.478025499999962,51.02782489999994],[4.478079099999993,51.027873899999896],[4.47801040000006,51.027903799999955],[4.477944199999975,51.02783550000022]]) |      * expect(copy.geometry.coordinates[0]).deep.equal([[4.477944199999975,51.02783550000022],[4.477987899999996,51.027818800000034],[4.478004500000021,51.02783399999988],[4.478025499999962,51.02782489999994],[4.478079099999993,51.027873899999896],[4.47801040000006,51.027903799999955],[4.477944199999975,51.02783550000022]]) | ||||||
|  | @ -569,7 +572,7 @@ export class GeoOperations { | ||||||
| 
 | 
 | ||||||
|         const copy = { |         const copy = { | ||||||
|             ...feature, |             ...feature, | ||||||
|             geometry: {...feature.geometry} |             geometry: { ...feature.geometry }, | ||||||
|         } |         } | ||||||
|         let coordinates: [number, number][] |         let coordinates: [number, number][] | ||||||
|         if (feature.geometry.type === "LineString") { |         if (feature.geometry.type === "LineString") { | ||||||
|  | @ -582,7 +585,7 @@ export class GeoOperations { | ||||||
| 
 | 
 | ||||||
|         // inline replacement in the coordinates list
 |         // inline replacement in the coordinates list
 | ||||||
|         for (let i = coordinates.length - 2; i >= 1; i--) { |         for (let i = coordinates.length - 2; i >= 1; i--) { | ||||||
|             const coordinate = coordinates[i]; |             const coordinate = coordinates[i] | ||||||
|             const nextCoordinate = coordinates[i + 1] |             const nextCoordinate = coordinates[i + 1] | ||||||
|             const prevCoordinate = 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
 |                 // In case that the line is going south, e.g. bearingN = 179, bearingP = -179
 | ||||||
|                 coordinates.splice(i, 1) |                 coordinates.splice(i, 1) | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
|         return copy; |         return copy | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static pointWithinRing(x: number, y: number, ring: [number, number][]) { |     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++) { |         for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { | ||||||
|             const coori = ring[i]; |             const coori = ring[i] | ||||||
|             const coorj = ring[j]; |             const coorj = ring[j] | ||||||
| 
 | 
 | ||||||
|             const xi = coori[0]; |             const xi = coori[0] | ||||||
|             const yi = coori[1]; |             const yi = coori[1] | ||||||
|             const xj = coorj[0]; |             const xj = coorj[0] | ||||||
|             const yj = coorj[1]; |             const yj = coorj[1] | ||||||
| 
 | 
 | ||||||
|             const intersect = ((yi > y) != (yj > y)) |             const intersect = yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi | ||||||
|                 && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); |  | ||||||
|             if (intersect) { |             if (intersect) { | ||||||
|                 inside = !inside; |                 inside = !inside | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return inside; |         return inside | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -642,46 +642,47 @@ export class GeoOperations { | ||||||
|      * Returns 0 if both are linestrings |      * Returns 0 if both are linestrings | ||||||
|      * Returns null if the features are not intersecting |      * 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") { |         if (feature.geometry.type === "LineString") { | ||||||
| 
 |             otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature) | ||||||
| 
 |  | ||||||
|             otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature); |  | ||||||
|             const overlaps = featureBBox.overlapsWith(otherFeatureBBox) |             const overlaps = featureBBox.overlapsWith(otherFeatureBBox) | ||||||
|             if (!overlaps) { |             if (!overlaps) { | ||||||
|                 return null; |                 return null | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Calculate the length of the intersection
 |             // Calculate the length of the intersection
 | ||||||
| 
 | 
 | ||||||
| 
 |             let intersectionPoints = turf.lineIntersect(feature, otherFeature) | ||||||
|             let intersectionPoints = turf.lineIntersect(feature, otherFeature); |  | ||||||
|             if (intersectionPoints.features.length == 0) { |             if (intersectionPoints.features.length == 0) { | ||||||
|                 // No intersections.
 |                 // No intersections.
 | ||||||
|                 // If one point is inside of the polygon, all points are
 |                 // 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] |                 const startCoor = coors[0] | ||||||
|                 if (this.inside(startCoor, otherFeature)) { |                 if (this.inside(startCoor, otherFeature)) { | ||||||
|                     return this.lengthInMeters(feature) |                     return this.lengthInMeters(feature) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 return null; |                 return null | ||||||
|             } |             } | ||||||
|             let intersectionPointsArray = intersectionPoints.features.map(d => { |             let intersectionPointsArray = intersectionPoints.features.map((d) => { | ||||||
|                 return d.geometry.coordinates |                 return d.geometry.coordinates | ||||||
|             }); |             }) | ||||||
| 
 | 
 | ||||||
|             if (otherFeature.geometry.type === "LineString") { |             if (otherFeature.geometry.type === "LineString") { | ||||||
|                 if (intersectionPointsArray.length > 0) { |                 if (intersectionPointsArray.length > 0) { | ||||||
|                     return 0 |                     return 0 | ||||||
|                 } |                 } | ||||||
|                 return null; |                 return null | ||||||
|             } |             } | ||||||
|             if (intersectionPointsArray.length == 1) { |             if (intersectionPointsArray.length == 1) { | ||||||
|                 // We need to add the start- or endpoint of the current feature, depending on which one is embedded
 |                 // 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] |                 const startCoor = coors[0] | ||||||
|                 if (this.inside(startCoor, otherFeature)) { |                 if (this.inside(startCoor, otherFeature)) { | ||||||
|                     // The startpoint is embedded
 |                     // 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) { |             if (intersection == null) { | ||||||
|                 return null; |                 return null | ||||||
|             } |             } | ||||||
|             const intersectionSize = turf.length(intersection); // in km
 |             const intersectionSize = turf.length(intersection) // in km
 | ||||||
|             return intersectionSize * 1000 |             return intersectionSize * 1000 | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") { |         if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") { | ||||||
|             const otherFeatureBBox = BBox.get(otherFeature); |             const otherFeatureBBox = BBox.get(otherFeature) | ||||||
|             const overlaps = featureBBox.overlapsWith(otherFeatureBBox) |             const overlaps = featureBBox.overlapsWith(otherFeatureBBox) | ||||||
|             if (!overlaps) { |             if (!overlaps) { | ||||||
|                 return null; |                 return null | ||||||
|             } |             } | ||||||
|             if (otherFeature.geometry.type === "LineString") { |             if (otherFeature.geometry.type === "LineString") { | ||||||
|                 return this.calculateInstersection(otherFeature, feature, otherFeatureBBox, featureBBox) |                 return this.calculateInstersection( | ||||||
|  |                     otherFeature, | ||||||
|  |                     feature, | ||||||
|  |                     otherFeatureBBox, | ||||||
|  |                     featureBBox | ||||||
|  |                 ) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             try { |             try { | ||||||
| 
 |                 const intersection = turf.intersect(feature, otherFeature) | ||||||
|                 const intersection = turf.intersect(feature, otherFeature); |  | ||||||
|                 if (intersection == null) { |                 if (intersection == null) { | ||||||
|                     return null; |                     return null | ||||||
|                 } |                 } | ||||||
|                 return turf.area(intersection); // in m²
 |                 return turf.area(intersection) // in m²
 | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 if (e.message === "Each LinearRing of a Polygon must have 4 or more Positions.") { |                 if (e.message === "Each LinearRing of a Polygon must have 4 or more Positions.") { | ||||||
|                     // WORKAROUND TIME!
 |                     // WORKAROUND TIME!
 | ||||||
|                     // See https://github.com/Turfjs/turf/pull/2238
 |                     // 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" |         throw "CalculateIntersection fallthrough: can not calculate an intersection between features" | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -742,7 +747,7 @@ export class GeoOperations { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns 'true' if one feature contains the other feature |      * Returns 'true' if one feature contains the other feature | ||||||
|      *  |      * | ||||||
|      * const pond: Feature<Polygon, any> = { |      * const pond: Feature<Polygon, any> = { | ||||||
|      *       "type": "Feature", |      *       "type": "Feature", | ||||||
|      *       "properties": {"natural":"water","water":"pond"}, |      *       "properties": {"natural":"water","water":"pond"}, | ||||||
|  | @ -769,9 +774,10 @@ export class GeoOperations { | ||||||
|      * GeoOperations.completelyWithin(pond, park) // => true
 |      * GeoOperations.completelyWithin(pond, park) // => true
 | ||||||
|      * GeoOperations.completelyWithin(park, pond) // => false
 |      * GeoOperations.completelyWithin(park, pond) // => false
 | ||||||
|      */ |      */ | ||||||
|     static completelyWithin(feature: Feature<Geometry, any>, possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any>) : boolean { |     static completelyWithin( | ||||||
|         return booleanWithin(feature, possiblyEncloingFeature); |         feature: Feature<Geometry, any>, | ||||||
|  |         possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any> | ||||||
|  |     ): boolean { | ||||||
|  |         return booleanWithin(feature, possiblyEncloingFeature) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -1,45 +1,52 @@ | ||||||
| import {Mapillary} from "./Mapillary"; | import { Mapillary } from "./Mapillary" | ||||||
| import {WikimediaImageProvider} from "./WikimediaImageProvider"; | import { WikimediaImageProvider } from "./WikimediaImageProvider" | ||||||
| import {Imgur} from "./Imgur"; | import { Imgur } from "./Imgur" | ||||||
| import GenericImageProvider from "./GenericImageProvider"; | import GenericImageProvider from "./GenericImageProvider" | ||||||
| import {Store, UIEventSource} from "../UIEventSource"; | import { Store, UIEventSource } from "../UIEventSource" | ||||||
| import ImageProvider, {ProvidedImage} from "./ImageProvider"; | import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||||
| import {WikidataImageProvider} from "./WikidataImageProvider"; | import { WikidataImageProvider } from "./WikidataImageProvider" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * A generic 'from the interwebz' image picker, without attribution |  * A generic 'from the interwebz' image picker, without attribution | ||||||
|  */ |  */ | ||||||
| export default class AllImageProviders { | export default class AllImageProviders { | ||||||
| 
 |  | ||||||
|     public static ImageAttributionSource: ImageProvider[] = [ |     public static ImageAttributionSource: ImageProvider[] = [ | ||||||
|         Imgur.singleton, |         Imgur.singleton, | ||||||
|         Mapillary.singleton, |         Mapillary.singleton, | ||||||
|         WikidataImageProvider.singleton, |         WikidataImageProvider.singleton, | ||||||
|         WikimediaImageProvider.singleton, |         WikimediaImageProvider.singleton, | ||||||
|         new GenericImageProvider( |         new GenericImageProvider( | ||||||
|             [].concat(...Imgur.defaultValuePrefix, ...WikimediaImageProvider.commonsPrefixes, ...Mapillary.valuePrefixes) |             [].concat( | ||||||
|         ) |                 ...Imgur.defaultValuePrefix, | ||||||
|  |                 ...WikimediaImageProvider.commonsPrefixes, | ||||||
|  |                 ...Mapillary.valuePrefixes | ||||||
|  |             ) | ||||||
|  |         ), | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     private static providersByName= { |     private static providersByName = { | ||||||
|         "imgur": Imgur.singleton, |         imgur: Imgur.singleton, | ||||||
| "mapillary":        Mapillary.singleton, |         mapillary: Mapillary.singleton, | ||||||
|      "wikidata":  WikidataImageProvider.singleton, |         wikidata: WikidataImageProvider.singleton, | ||||||
|        "wikimedia": WikimediaImageProvider.singleton |         wikimedia: WikimediaImageProvider.singleton, | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     public static byName(name: string){ |     public static byName(name: string) { | ||||||
|         return AllImageProviders.providersByName[name.toLowerCase()] |         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< | ||||||
|     private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<string, UIEventSource<ProvidedImage[]>>() |         string, | ||||||
|  |         UIEventSource<ProvidedImage[]> | ||||||
|  |     >() | ||||||
| 
 | 
 | ||||||
|     public static LoadImagesFor(tags: Store<any>, tagKey?: string[]): Store<ProvidedImage[]> { |     public static LoadImagesFor(tags: Store<any>, tagKey?: string[]): Store<ProvidedImage[]> { | ||||||
|         if (tags.data.id === undefined) { |         if (tags.data.id === undefined) { | ||||||
|             return undefined; |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const cacheKey = tags.data.id + tagKey |         const cacheKey = tags.data.id + tagKey | ||||||
|  | @ -48,23 +55,21 @@ export default class AllImageProviders { | ||||||
|             return cached |             return cached | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         const source = new UIEventSource([]) |         const source = new UIEventSource([]) | ||||||
|         this._cache.set(cacheKey, source) |         this._cache.set(cacheKey, source) | ||||||
|         const allSources = [] |         const allSources = [] | ||||||
|         for (const imageProvider of AllImageProviders.ImageAttributionSource) { |         for (const imageProvider of AllImageProviders.ImageAttributionSource) { | ||||||
| 
 |  | ||||||
|             let prefixes = imageProvider.defaultKeyPrefixes |             let prefixes = imageProvider.defaultKeyPrefixes | ||||||
|             if (tagKey !== undefined) { |             if (tagKey !== undefined) { | ||||||
|                 prefixes = tagKey |                 prefixes = tagKey | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const singleSource = imageProvider.GetRelevantUrls(tags, { |             const singleSource = imageProvider.GetRelevantUrls(tags, { | ||||||
|                 prefixes: prefixes |                 prefixes: prefixes, | ||||||
|             }) |             }) | ||||||
|             allSources.push(singleSource) |             allSources.push(singleSource) | ||||||
|             singleSource.addCallbackAndRunD(_ => { |             singleSource.addCallbackAndRunD((_) => { | ||||||
|                 const all: ProvidedImage[] = [].concat(...allSources.map(source => source.data)) |                 const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data)) | ||||||
|                 const uniq = [] |                 const uniq = [] | ||||||
|                 const seen = new Set<string>() |                 const seen = new Set<string>() | ||||||
|                 for (const img of all) { |                 for (const img of all) { | ||||||
|  | @ -77,7 +82,6 @@ export default class AllImageProviders { | ||||||
|                 source.setData(uniq) |                 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 { | 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[]) { |     public constructor(valuePrefixBlacklist: string[]) { | ||||||
|         super(); |         super() | ||||||
|         this._valuePrefixBlacklist = valuePrefixBlacklist; |         this._valuePrefixBlacklist = valuePrefixBlacklist | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { |     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 [] |             return [] | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -23,20 +22,20 @@ export default class GenericImageProvider extends ImageProvider { | ||||||
|             return [] |             return [] | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return [Promise.resolve({ |         return [ | ||||||
|             key: key, |             Promise.resolve({ | ||||||
|             url: value, |                 key: key, | ||||||
|             provider: this |                 url: value, | ||||||
|         })] |                 provider: this, | ||||||
|  |             }), | ||||||
|  |         ] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     SourceIcon(backlinkSource?: string) { |     SourceIcon(backlinkSource?: string) { | ||||||
|         return undefined; |         return undefined | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public DownloadAttribution(url: string) { |     public DownloadAttribution(url: string) { | ||||||
|         return undefined |         return undefined | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,50 +1,53 @@ | ||||||
| import {Store, UIEventSource} from "../UIEventSource"; | import { Store, UIEventSource } from "../UIEventSource" | ||||||
| import BaseUIElement from "../../UI/BaseUIElement"; | import BaseUIElement from "../../UI/BaseUIElement" | ||||||
| import {LicenseInfo} from "./LicenseInfo"; | import { LicenseInfo } from "./LicenseInfo" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| export interface ProvidedImage { | export interface ProvidedImage { | ||||||
|     url: string, |     url: string | ||||||
|     key: string, |     key: string | ||||||
|     provider: ImageProvider |     provider: ImageProvider | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default abstract class ImageProvider { | export default abstract class ImageProvider { | ||||||
| 
 |  | ||||||
|     public abstract readonly defaultKeyPrefixes: string[] |     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 |      * Given a properies object, maps it onto _all_ the available pictures for this imageProvider | ||||||
|      */ |      */ | ||||||
|     public GetRelevantUrls(allTags: Store<any>, options?: { |     public GetRelevantUrls( | ||||||
|         prefixes?: string[] |         allTags: Store<any>, | ||||||
|     }): UIEventSource<ProvidedImage[]> { |         options?: { | ||||||
|  |             prefixes?: string[] | ||||||
|  |         } | ||||||
|  |     ): UIEventSource<ProvidedImage[]> { | ||||||
|         const prefixes = options?.prefixes ?? this.defaultKeyPrefixes |         const prefixes = options?.prefixes ?? this.defaultKeyPrefixes | ||||||
|         if (prefixes === undefined) { |         if (prefixes === undefined) { | ||||||
|             throw "No `defaultKeyPrefixes` defined by this image provider" |             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>() |         const seenValues = new Set<string>() | ||||||
|         allTags.addCallbackAndRunD(tags => { |         allTags.addCallbackAndRunD((tags) => { | ||||||
|             for (const key in tags) { |             for (const key in tags) { | ||||||
|                 if (!prefixes.some(prefix => key.startsWith(prefix))) { |                 if (!prefixes.some((prefix) => key.startsWith(prefix))) { | ||||||
|                     continue |                     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) { |                 for (const value of values) { | ||||||
| 
 |  | ||||||
|                     if (seenValues.has(value)) { |                     if (seenValues.has(value)) { | ||||||
|                         continue |                         continue | ||||||
|                     } |                     } | ||||||
|                     seenValues.add(value) |                     seenValues.add(value) | ||||||
|                     this.ExtractUrls(key, value).then(promises => { |                     this.ExtractUrls(key, value).then((promises) => { | ||||||
|                         for (const promise of promises ?? []) { |                         for (const promise of promises ?? []) { | ||||||
|                             if (promise === undefined) { |                             if (promise === undefined) { | ||||||
|                                 continue |                                 continue | ||||||
|                             } |                             } | ||||||
|                             promise.then(providedImage => { |                             promise.then((providedImage) => { | ||||||
|                                 if (providedImage === undefined) { |                                 if (providedImage === undefined) { | ||||||
|                                     return |                                     return | ||||||
|                                 } |                                 } | ||||||
|  | @ -54,15 +57,12 @@ export default abstract class ImageProvider { | ||||||
|                         } |                         } | ||||||
|                     }) |                     }) | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|         return relevantUrls |         return relevantUrls | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>; |     public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> | ||||||
| 
 | 
 | ||||||
|     public abstract DownloadAttribution(url: string): Promise<LicenseInfo>; |     public abstract DownloadAttribution(url: string): Promise<LicenseInfo> | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,93 +1,105 @@ | ||||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider"; | import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||||
| import BaseUIElement from "../../UI/BaseUIElement"; | import BaseUIElement from "../../UI/BaseUIElement" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants" | ||||||
| import {LicenseInfo} from "./LicenseInfo"; | import { LicenseInfo } from "./LicenseInfo" | ||||||
| 
 | 
 | ||||||
| export class Imgur extends ImageProvider { | export class Imgur extends ImageProvider { | ||||||
| 
 |  | ||||||
|     public static readonly defaultValuePrefix = ["https://i.imgur.com"] |     public static readonly defaultValuePrefix = ["https://i.imgur.com"] | ||||||
|     public static readonly singleton = new Imgur(); |     public static readonly singleton = new Imgur() | ||||||
|     public readonly defaultKeyPrefixes: string[] = ["image"]; |     public readonly defaultKeyPrefixes: string[] = ["image"] | ||||||
| 
 | 
 | ||||||
|     private constructor() { |     private constructor() { | ||||||
|         super(); |         super() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static uploadMultiple( |     static uploadMultiple( | ||||||
|         title: string, description: string, blobs: FileList, |         title: string, | ||||||
|         handleSuccessfullUpload: ((imageURL: string) => Promise<void>), |         description: string, | ||||||
|         allDone: (() => void), |         blobs: FileList, | ||||||
|         onFail: ((reason: string) => void), |         handleSuccessfullUpload: (imageURL: string) => Promise<void>, | ||||||
|         offset: number = 0) { |         allDone: () => void, | ||||||
| 
 |         onFail: (reason: string) => void, | ||||||
|  |         offset: number = 0 | ||||||
|  |     ) { | ||||||
|         if (blobs.length == offset) { |         if (blobs.length == offset) { | ||||||
|             allDone(); |             allDone() | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         const blob = blobs.item(offset); |         const blob = blobs.item(offset) | ||||||
|         const self = this; |         const self = this | ||||||
|         this.uploadImage(title, description, blob, |         this.uploadImage( | ||||||
|  |             title, | ||||||
|  |             description, | ||||||
|  |             blob, | ||||||
|             async (imageUrl) => { |             async (imageUrl) => { | ||||||
|                 await handleSuccessfullUpload(imageUrl); |                 await handleSuccessfullUpload(imageUrl) | ||||||
|                 self.uploadMultiple( |                 self.uploadMultiple( | ||||||
|                     title, description, blobs, |                     title, | ||||||
|  |                     description, | ||||||
|  |                     blobs, | ||||||
|                     handleSuccessfullUpload, |                     handleSuccessfullUpload, | ||||||
|                     allDone, |                     allDone, | ||||||
|                     onFail, |                     onFail, | ||||||
|                     offset + 1); |                     offset + 1 | ||||||
|  |                 ) | ||||||
|             }, |             }, | ||||||
|             onFail |             onFail | ||||||
|         ); |         ) | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static uploadImage(title: string, description: string, blob: File, |     static uploadImage( | ||||||
|                        handleSuccessfullUpload: ((imageURL: string) => Promise<void>), |         title: string, | ||||||
|                        onFail: (reason: string) => void) { |         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 formData = new FormData() | ||||||
|         const apiKey = Constants.ImgurApiKey; |         formData.append("image", blob) | ||||||
| 
 |         formData.append("title", title) | ||||||
|         const formData = new FormData(); |  | ||||||
|         formData.append('image', blob); |  | ||||||
|         formData.append("title", title); |  | ||||||
|         formData.append("description", description) |         formData.append("description", description) | ||||||
| 
 | 
 | ||||||
|         const settings: RequestInit = { |         const settings: RequestInit = { | ||||||
|             method: 'POST', |             method: "POST", | ||||||
|             body: formData, |             body: formData, | ||||||
|             redirect: 'follow', |             redirect: "follow", | ||||||
|             headers: new Headers({ |             headers: new Headers({ | ||||||
|                 Authorization: `Client-ID ${apiKey}`, |                 Authorization: `Client-ID ${apiKey}`, | ||||||
|                 Accept: 'application/json', |                 Accept: "application/json", | ||||||
|             }), |             }), | ||||||
|         }; |         } | ||||||
| 
 | 
 | ||||||
|         // Response contains stringified JSON
 |         // Response contains stringified JSON
 | ||||||
|         // Image URL available at response.data.link
 |         // Image URL available at response.data.link
 | ||||||
|         fetch(apiUrl, settings).then(async function (response) { |         fetch(apiUrl, settings) | ||||||
|             const content = await response.json() |             .then(async function (response) { | ||||||
|             await handleSuccessfullUpload(content.data.link); |                 const content = await response.json() | ||||||
|         }).catch((reason) => { |                 await handleSuccessfullUpload(content.data.link) | ||||||
|             console.log("Uploading to IMGUR failed", reason); |             }) | ||||||
|             // @ts-ignore
 |             .catch((reason) => { | ||||||
|             onFail(reason); |                 console.log("Uploading to IMGUR failed", reason) | ||||||
|         }); |                 // @ts-ignore
 | ||||||
|  |                 onFail(reason) | ||||||
|  |             }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     SourceIcon(): BaseUIElement { |     SourceIcon(): BaseUIElement { | ||||||
|         return undefined; |         return undefined | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { |     public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { | ||||||
|         if (Imgur.defaultValuePrefix.some(prefix => value.startsWith(prefix))) { |         if (Imgur.defaultValuePrefix.some((prefix) => value.startsWith(prefix))) { | ||||||
|             return [Promise.resolve({ |             return [ | ||||||
|                 url: value, |                 Promise.resolve({ | ||||||
|                 key: key, |                     url: value, | ||||||
|                 provider: this |                     key: key, | ||||||
|             })] |                     provider: this, | ||||||
|  |                 }), | ||||||
|  |             ] | ||||||
|         } |         } | ||||||
|         return [] |         return [] | ||||||
|     } |     } | ||||||
|  | @ -103,29 +115,27 @@ export class Imgur extends ImageProvider { | ||||||
|      * expected.artist = "Pieter Vander Vennet" |      * expected.artist = "Pieter Vander Vennet" | ||||||
|      * licenseInfo // => expected
 |      * licenseInfo // => expected
 | ||||||
|      */ |      */ | ||||||
|     public async DownloadAttribution (url: string) : Promise<LicenseInfo> { |     public async DownloadAttribution(url: string): Promise<LicenseInfo> { | ||||||
|         const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0]; |         const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0] | ||||||
| 
 | 
 | ||||||
|         const apiUrl = 'https://api.imgur.com/3/image/' + hash; |         const apiUrl = "https://api.imgur.com/3/image/" + hash | ||||||
|         const response = await Utils.downloadJsonCached(apiUrl, 365*24*60*60, |         const response = await Utils.downloadJsonCached(apiUrl, 365 * 24 * 60 * 60, { | ||||||
|             {Authorization: 'Client-ID ' + Constants.ImgurApiKey}) |             Authorization: "Client-ID " + Constants.ImgurApiKey, | ||||||
|  |         }) | ||||||
| 
 | 
 | ||||||
|         const descr: string = response.data.description ?? ""; |         const descr: string = response.data.description ?? "" | ||||||
|         const data: any = {}; |         const data: any = {} | ||||||
|         for (const tag of descr.split("\n")) { |         for (const tag of descr.split("\n")) { | ||||||
|             const kv = tag.split(":"); |             const kv = tag.split(":") | ||||||
|             const k = kv[0]; |             const k = kv[0] | ||||||
|             data[k] = kv[1]?.replace(/\r/g, ""); |             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 |         return licenseInfo | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,16 +1,15 @@ | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import {Imgur} from "./Imgur"; | import { Imgur } from "./Imgur" | ||||||
| 
 | 
 | ||||||
| export default class ImgurUploader { | export default class ImgurUploader { | ||||||
| 
 |     public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([]) | ||||||
|     public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([]); |     public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([]) | ||||||
|     public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([]); |     public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([]) | ||||||
|     public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([]); |     public maxFileSizeInMegabytes = 10 | ||||||
|     public maxFileSizeInMegabytes = 10; |     private readonly _handleSuccessUrl: (string) => Promise<void> | ||||||
|     private readonly _handleSuccessUrl: (string) => Promise<void>; |  | ||||||
| 
 | 
 | ||||||
|     constructor(handleSuccessUrl: (string) => Promise<void>) { |     constructor(handleSuccessUrl: (string) => Promise<void>) { | ||||||
|         this._handleSuccessUrl = handleSuccessUrl; |         this._handleSuccessUrl = handleSuccessUrl | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public uploadMany(title: string, description: string, files: FileList): void { |     public uploadMany(title: string, description: string, files: FileList): void { | ||||||
|  | @ -19,25 +18,26 @@ export default class ImgurUploader { | ||||||
|         } |         } | ||||||
|         this.queue.ping() |         this.queue.ping() | ||||||
| 
 | 
 | ||||||
|         const self = this; |         const self = this | ||||||
|         this.queue.setData([...self.queue.data]) |         this.queue.setData([...self.queue.data]) | ||||||
|         Imgur.uploadMultiple(title, |         Imgur.uploadMultiple( | ||||||
|  |             title, | ||||||
|             description, |             description, | ||||||
|             files, |             files, | ||||||
|             async function (url) { |             async function (url) { | ||||||
|                 console.log("File saved at", url); |                 console.log("File saved at", url) | ||||||
|                 self.success.data.push(url) |                 self.success.data.push(url) | ||||||
|                 self.success.ping(); |                 self.success.ping() | ||||||
|                 await self._handleSuccessUrl(url); |                 await self._handleSuccessUrl(url) | ||||||
|             }, |             }, | ||||||
|             function () { |             function () { | ||||||
|                 console.log("All uploads completed"); |                 console.log("All uploads completed") | ||||||
|             }, |             }, | ||||||
| 
 | 
 | ||||||
|             function (failReason) { |             function (failReason) { | ||||||
|                 console.log("Upload failed due to ", failReason) |                 console.log("Upload failed due to ", failReason) | ||||||
|                 self.failed.setData([...self.failed.data, failReason]) |                 self.failed.setData([...self.failed.data, failReason]) | ||||||
|             } |             } | ||||||
|         ); |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,12 +1,12 @@ | ||||||
| export class LicenseInfo { | export class LicenseInfo { | ||||||
|     title: string = "" |     title: string = "" | ||||||
|     artist: string = ""; |     artist: string = "" | ||||||
|     license: string = undefined; |     license: string = undefined | ||||||
|     licenseShortName: string = ""; |     licenseShortName: string = "" | ||||||
|     usageTerms: string = ""; |     usageTerms: string = "" | ||||||
|     attributionRequired: boolean = false; |     attributionRequired: boolean = false | ||||||
|     copyrighted: boolean = false; |     copyrighted: boolean = false | ||||||
|     credit: string = ""; |     credit: string = "" | ||||||
|     description: string = ""; |     description: string = "" | ||||||
|     informationLocation: URL = undefined |     informationLocation: URL = undefined | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,21 +1,26 @@ | ||||||
| import ImageProvider, {ProvidedImage} from "./ImageProvider"; | import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||||
| import BaseUIElement from "../../UI/BaseUIElement"; | import BaseUIElement from "../../UI/BaseUIElement" | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import {LicenseInfo} from "./LicenseInfo"; | import { LicenseInfo } from "./LicenseInfo" | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants" | ||||||
| 
 | 
 | ||||||
| export class Mapillary extends ImageProvider { | 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" |     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"] |     defaultKeyPrefixes = ["mapillary", "image"] | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Indicates that this is the same URL |      * Indicates that this is the same URL | ||||||
|      * Ignores 'stp' parameter |      * Ignores 'stp' parameter | ||||||
|      *  |      * | ||||||
|      * const a = "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8xm5SGLt20ETziNqzhhBd8b8S5GHLiIu8N6BbyqHFohFAQoaJJPG8i5yQiSwjYmEqXSfVeoCmpiyBJICEkQK98JOB21kkJoBS8VdhYa-Ty93lBnznQyesJBtKcb32foGut2Hgt10hEMWJbE3dDgA?stp=s1024x768&ccb=10-5&oh=00_AT-ZGTXHzihoaQYBILmEiAEKR64z_IWiTlcAYq_D7Ka0-Q&oe=6278C456&_nc_sid=122ab1" |      * const a = "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8xm5SGLt20ETziNqzhhBd8b8S5GHLiIu8N6BbyqHFohFAQoaJJPG8i5yQiSwjYmEqXSfVeoCmpiyBJICEkQK98JOB21kkJoBS8VdhYa-Ty93lBnznQyesJBtKcb32foGut2Hgt10hEMWJbE3dDgA?stp=s1024x768&ccb=10-5&oh=00_AT-ZGTXHzihoaQYBILmEiAEKR64z_IWiTlcAYq_D7Ka0-Q&oe=6278C456&_nc_sid=122ab1" | ||||||
|      * const b = "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8xm5SGLt20ETziNqzhhBd8b8S5GHLiIu8N6BbyqHFohFAQoaJJPG8i5yQiSwjYmEqXSfVeoCmpiyBJICEkQK98JOB21kkJoBS8VdhYa-Ty93lBnznQyesJBtKcb32foGut2Hgt10hEMWJbE3dDgA?stp=s256x192&ccb=10-5&oh=00_AT9BZ1Rpc9zbY_uNu92A_4gj1joiy1b6VtgtLIu_7wh9Bg&oe=6278C456&_nc_sid=122ab1" |      * const b = "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8xm5SGLt20ETziNqzhhBd8b8S5GHLiIu8N6BbyqHFohFAQoaJJPG8i5yQiSwjYmEqXSfVeoCmpiyBJICEkQK98JOB21kkJoBS8VdhYa-Ty93lBnznQyesJBtKcb32foGut2Hgt10hEMWJbE3dDgA?stp=s256x192&ccb=10-5&oh=00_AT9BZ1Rpc9zbY_uNu92A_4gj1joiy1b6VtgtLIu_7wh9Bg&oe=6278C456&_nc_sid=122ab1" | ||||||
|      * Mapillary.sameUrl(a, b) => true |      * Mapillary.sameUrl(a, b) => true | ||||||
|  | @ -28,9 +33,9 @@ export class Mapillary extends ImageProvider { | ||||||
|             const aUrl = new URL(a) |             const aUrl = new URL(a) | ||||||
|             const bUrl = new URL(b) |             const bUrl = new URL(b) | ||||||
|             if (aUrl.host !== bUrl.host || aUrl.pathname !== bUrl.pathname) { |             if (aUrl.host !== bUrl.host || aUrl.pathname !== bUrl.pathname) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|             let allSame = true; |             let allSame = true | ||||||
|             aUrl.searchParams.forEach((value, key) => { |             aUrl.searchParams.forEach((value, key) => { | ||||||
|                 if (key === "stp") { |                 if (key === "stp") { | ||||||
|                     // This is the key indicating the image size on mapillary; we ignore it
 |                     // This is the key indicating the image size on mapillary; we ignore it
 | ||||||
|  | @ -41,20 +46,18 @@ export class Mapillary extends ImageProvider { | ||||||
|                     return |                     return | ||||||
|                 } |                 } | ||||||
|             }) |             }) | ||||||
|             return allSame; |             return allSame | ||||||
| 
 |  | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.debug("Could not compare ", a, "and", b, "due to", e) |             console.debug("Could not compare ", a, "and", b, "due to", e) | ||||||
|         } |         } | ||||||
|         return false; |         return false | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns the correct key for API v4.0 |      * Returns the correct key for API v4.0 | ||||||
|      */ |      */ | ||||||
|     private static ExtractKeyFromURL(value: string): number { |     private static ExtractKeyFromURL(value: string): number { | ||||||
|         let key: string; |         let key: string | ||||||
| 
 | 
 | ||||||
|         const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/) |         const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/) | ||||||
|         if (newApiFormat !== null) { |         if (newApiFormat !== null) { | ||||||
|  | @ -62,7 +65,7 @@ export class Mapillary extends ImageProvider { | ||||||
|         } else if (value.startsWith(Mapillary.valuePrefix)) { |         } else if (value.startsWith(Mapillary.valuePrefix)) { | ||||||
|             key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1) |             key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1) | ||||||
|         } else if (value.match("[0-9]*")) { |         } else if (value.match("[0-9]*")) { | ||||||
|             key = value; |             key = value | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const keyAsNumber = Number(key) |         const keyAsNumber = Number(key) | ||||||
|  | @ -74,7 +77,7 @@ export class Mapillary extends ImageProvider { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     SourceIcon(backlinkSource?: string): BaseUIElement { |     SourceIcon(backlinkSource?: string): BaseUIElement { | ||||||
|         return Svg.mapillary_svg(); |         return Svg.mapillary_svg() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { |     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> { |     public async DownloadAttribution(url: string): Promise<LicenseInfo> { | ||||||
|         const license = new LicenseInfo() |         const license = new LicenseInfo() | ||||||
|         license.artist = "Contributor name unavailable"; |         license.artist = "Contributor name unavailable" | ||||||
|         license.license = "CC BY-SA 4.0"; |         license.license = "CC BY-SA 4.0" | ||||||
|         // license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
 |         // license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
 | ||||||
|         license.attributionRequired = true; |         license.attributionRequired = true | ||||||
|         return license |         return license | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async PrepareUrlAsync(key: string, value: string): Promise<ProvidedImage> { |     private async PrepareUrlAsync(key: string, value: string): Promise<ProvidedImage> { | ||||||
|         const mapillaryId = Mapillary.ExtractKeyFromURL(value) |         const mapillaryId = Mapillary.ExtractKeyFromURL(value) | ||||||
|         if (mapillaryId === undefined) { |         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 metadataUrl = | ||||||
|         const response = await Utils.downloadJsonCached(metadataUrl,60*60) |             "https://graph.mapillary.com/" + | ||||||
|         const url = <string>response["thumb_1024_url"]; |             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 { |         return { | ||||||
|             url: url, |             url: url, | ||||||
|             provider: this, |             provider: this, | ||||||
|             key: key |             key: key, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,11 +1,10 @@ | ||||||
| import ImageProvider, {ProvidedImage} from "./ImageProvider"; | import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||||
| import BaseUIElement from "../../UI/BaseUIElement"; | import BaseUIElement from "../../UI/BaseUIElement" | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg" | ||||||
| import {WikimediaImageProvider} from "./WikimediaImageProvider"; | import { WikimediaImageProvider } from "./WikimediaImageProvider" | ||||||
| import Wikidata from "../Web/Wikidata"; | import Wikidata from "../Web/Wikidata" | ||||||
| 
 | 
 | ||||||
| export class WikidataImageProvider extends ImageProvider { | export class WikidataImageProvider extends ImageProvider { | ||||||
| 
 |  | ||||||
|     public static readonly singleton = new WikidataImageProvider() |     public static readonly singleton = new WikidataImageProvider() | ||||||
|     public readonly defaultKeyPrefixes = ["wikidata"] |     public readonly defaultKeyPrefixes = ["wikidata"] | ||||||
| 
 | 
 | ||||||
|  | @ -14,7 +13,7 @@ export class WikidataImageProvider extends ImageProvider { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public SourceIcon(backlinkSource?: string): BaseUIElement { |     public SourceIcon(backlinkSource?: string): BaseUIElement { | ||||||
|         throw Svg.wikidata_svg(); |         throw Svg.wikidata_svg() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { |     public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { | ||||||
|  | @ -39,7 +38,10 @@ export class WikidataImageProvider extends ImageProvider { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const commons = entity.commons |         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) |             const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, commons) | ||||||
|             allImages.push(...promises) |             allImages.push(...promises) | ||||||
|         } |         } | ||||||
|  | @ -47,7 +49,6 @@ export class WikidataImageProvider extends ImageProvider { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public DownloadAttribution(url: string): Promise<any> { |     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 ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||||
| import BaseUIElement from "../../UI/BaseUIElement"; | import BaseUIElement from "../../UI/BaseUIElement" | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg" | ||||||
| import Link from "../../UI/Base/Link"; | import Link from "../../UI/Base/Link" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import {LicenseInfo} from "./LicenseInfo"; | import { LicenseInfo } from "./LicenseInfo" | ||||||
| import Wikimedia from "../Web/Wikimedia"; | import Wikimedia from "../Web/Wikimedia" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * This module provides endpoints for wikimedia and others |  * This module provides endpoints for wikimedia and others | ||||||
|  */ |  */ | ||||||
| export class WikimediaImageProvider extends ImageProvider { | export class WikimediaImageProvider extends ImageProvider { | ||||||
| 
 |     public static readonly singleton = new WikimediaImageProvider() | ||||||
| 
 |     public static readonly commonsPrefixes = [ | ||||||
|     public static readonly singleton = new WikimediaImageProvider(); |         "https://commons.wikimedia.org/wiki/", | ||||||
|     public static readonly commonsPrefixes = ["https://commons.wikimedia.org/wiki/", "https://upload.wikimedia.org", "File:"] |         "https://upload.wikimedia.org", | ||||||
|  |         "File:", | ||||||
|  |     ] | ||||||
|     private readonly commons_key = "wikimedia_commons" |     private readonly commons_key = "wikimedia_commons" | ||||||
|     public readonly defaultKeyPrefixes = [this.commons_key, "image"] |     public readonly defaultKeyPrefixes = [this.commons_key, "image"] | ||||||
| 
 | 
 | ||||||
|     private constructor() { |     private constructor() { | ||||||
|         super(); |         super() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static ExtractFileName(url: string) { |     private static ExtractFileName(url: string) { | ||||||
|         if (!url.startsWith("http")) { |         if (!url.startsWith("http")) { | ||||||
|             return url; |             return url | ||||||
|         } |         } | ||||||
|         const path = new URL(url).pathname |         const path = new URL(url).pathname | ||||||
|         return path.substring(path.lastIndexOf("/") + 1); |         return path.substring(path.lastIndexOf("/") + 1) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static PrepareUrl(value: string): string { |     private static PrepareUrl(value: string): string { | ||||||
| 
 |  | ||||||
|         if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) { |         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 { |     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 { |     private static removeCommonsPrefix(value: string): string { | ||||||
|  | @ -49,7 +51,7 @@ export class WikimediaImageProvider extends ImageProvider { | ||||||
|             if (!value.startsWith("File:")) { |             if (!value.startsWith("File:")) { | ||||||
|                 value = "File:" + value |                 value = "File:" + value | ||||||
|             } |             } | ||||||
|             return value; |             return value | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         for (const prefix of WikimediaImageProvider.commonsPrefixes) { |         for (const prefix of WikimediaImageProvider.commonsPrefixes) { | ||||||
|  | @ -61,21 +63,20 @@ export class WikimediaImageProvider extends ImageProvider { | ||||||
|                 return part |                 return part | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return value; |         return value | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     SourceIcon(backlink: string): BaseUIElement { |     SourceIcon(backlink: string): BaseUIElement { | ||||||
|         const img = Svg.wikimedia_commons_white_svg() |         const img = Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em") | ||||||
|             .SetStyle("width:2em;height: 2em"); |  | ||||||
|         if (backlink === undefined) { |         if (backlink === undefined) { | ||||||
|             return img |             return img | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |         return new Link( | ||||||
|         return new Link(Svg.wikimedia_commons_white_img, |             Svg.wikimedia_commons_white_img, | ||||||
|             `https://commons.wikimedia.org/wiki/${backlink}`, true) |             `https://commons.wikimedia.org/wiki/${backlink}`, | ||||||
| 
 |             true | ||||||
| 
 |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public PrepUrl(value: string): ProvidedImage { |     public PrepUrl(value: string): ProvidedImage { | ||||||
|  | @ -99,7 +100,9 @@ export class WikimediaImageProvider extends ImageProvider { | ||||||
|         value = WikimediaImageProvider.removeCommonsPrefix(value) |         value = WikimediaImageProvider.removeCommonsPrefix(value) | ||||||
|         if (value.startsWith("Category:")) { |         if (value.startsWith("Category:")) { | ||||||
|             const urls = await Wikimedia.GetCategoryContents(value) |             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:")) { |         if (value.startsWith("File:")) { | ||||||
|             return [Promise.resolve(this.UrlForImage(value))] |             return [Promise.resolve(this.UrlForImage(value))] | ||||||
|  | @ -116,24 +119,30 @@ export class WikimediaImageProvider extends ImageProvider { | ||||||
|         filename = WikimediaImageProvider.ExtractFileName(filename) |         filename = WikimediaImageProvider.ExtractFileName(filename) | ||||||
| 
 | 
 | ||||||
|         if (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&" + |             "api.php?action=query&prop=imageinfo&iiprop=extmetadata&" + | ||||||
|             "titles=" + filename + |             "titles=" + | ||||||
|             "&format=json&origin=*"; |             filename + | ||||||
|         const data = await Utils.downloadJsonCached(url,365*24*60*60) |             "&format=json&origin=*" | ||||||
|         const licenseInfo = new LicenseInfo(); |         const data = await Utils.downloadJsonCached(url, 365 * 24 * 60 * 60) | ||||||
|  |         const licenseInfo = new LicenseInfo() | ||||||
|         const pageInfo = data.query.pages[-1] |         const pageInfo = data.query.pages[-1] | ||||||
|         if (pageInfo === undefined) { |         if (pageInfo === undefined) { | ||||||
|             return undefined; |             return undefined | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         const license = (pageInfo.imageinfo ?? [])[0]?.extmetadata; |         const license = (pageInfo.imageinfo ?? [])[0]?.extmetadata | ||||||
|         if (license === undefined) { |         if (license === undefined) { | ||||||
|             console.warn("The file", filename, "has no usable metedata or license attached... Please fix the license info file yourself!") |             console.warn( | ||||||
|             return undefined; |                 "The file", | ||||||
|  |                 filename, | ||||||
|  |                 "has no usable metedata or license attached... Please fix the license info file yourself!" | ||||||
|  |             ) | ||||||
|  |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let title = pageInfo.title |         let title = pageInfo.title | ||||||
|  | @ -145,26 +154,22 @@ export class WikimediaImageProvider extends ImageProvider { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         licenseInfo.title = title |         licenseInfo.title = title | ||||||
|         licenseInfo.artist = license.Artist?.value; |         licenseInfo.artist = license.Artist?.value | ||||||
|         licenseInfo.license = license.License?.value; |         licenseInfo.license = license.License?.value | ||||||
|         licenseInfo.copyrighted = license.Copyrighted?.value; |         licenseInfo.copyrighted = license.Copyrighted?.value | ||||||
|         licenseInfo.attributionRequired = license.AttributionRequired?.value; |         licenseInfo.attributionRequired = license.AttributionRequired?.value | ||||||
|         licenseInfo.usageTerms = license.UsageTerms?.value; |         licenseInfo.usageTerms = license.UsageTerms?.value | ||||||
|         licenseInfo.licenseShortName = license.LicenseShortName?.value; |         licenseInfo.licenseShortName = license.LicenseShortName?.value | ||||||
|         licenseInfo.credit = license.Credit?.value; |         licenseInfo.credit = license.Credit?.value | ||||||
|         licenseInfo.description = license.ImageDescription?.value; |         licenseInfo.description = license.ImageDescription?.value | ||||||
|         licenseInfo.informationLocation = new URL("https://en.wikipedia.org/wiki/"+pageInfo.title) |         licenseInfo.informationLocation = new URL("https://en.wikipedia.org/wiki/" + pageInfo.title) | ||||||
|         return licenseInfo; |         return licenseInfo | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private UrlForImage(image: string): ProvidedImage { |     private UrlForImage(image: string): ProvidedImage { | ||||||
|         if (!image.startsWith("File:")) { |         if (!image.startsWith("File:")) { | ||||||
|             image = "File:" + image |             image = "File:" + image | ||||||
|         } |         } | ||||||
|         return {url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this} |         return { url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -1,39 +1,39 @@ | ||||||
| import Constants from "../Models/Constants"; | import Constants from "../Models/Constants" | ||||||
| 
 | 
 | ||||||
| export default class Maproulette { | export default class Maproulette { | ||||||
|   /** |     /** | ||||||
|    * The API endpoint to use |      * The API endpoint to use | ||||||
|    */ |      */ | ||||||
|   endpoint: string; |     endpoint: string | ||||||
| 
 | 
 | ||||||
|   /** |     /** | ||||||
|    * The API key to use for all requests |      * The API key to use for all requests | ||||||
|    */ |      */ | ||||||
|   private apiKey: string; |     private apiKey: string | ||||||
| 
 | 
 | ||||||
|   /** |     /** | ||||||
|    * Creates a new Maproulette instance |      * Creates a new Maproulette instance | ||||||
|    * @param endpoint The API endpoint to use |      * @param endpoint The API endpoint to use | ||||||
|    */ |      */ | ||||||
|   constructor(endpoint: string = "https://maproulette.org/api/v2") { |     constructor(endpoint: string = "https://maproulette.org/api/v2") { | ||||||
|     this.endpoint = endpoint; |         this.endpoint = endpoint | ||||||
|     this.apiKey = Constants.MaprouletteApiKey; |         this.apiKey = Constants.MaprouletteApiKey | ||||||
|   } |     } | ||||||
| 
 | 
 | ||||||
|   /** |     /** | ||||||
|    * Close a task |      * Close a task | ||||||
|    * @param taskId The task to close |      * @param taskId The task to close | ||||||
|    */ |      */ | ||||||
|   async closeTask(taskId: number): Promise<void> { |     async closeTask(taskId: number): Promise<void> { | ||||||
|     const response = await fetch(`${this.endpoint}/task/${taskId}/1`, { |         const response = await fetch(`${this.endpoint}/task/${taskId}/1`, { | ||||||
|       method: "PUT", |             method: "PUT", | ||||||
|       headers: { |             headers: { | ||||||
|         "Content-Type": "application/json", |                 "Content-Type": "application/json", | ||||||
|         "apiKey": this.apiKey, |                 apiKey: this.apiKey, | ||||||
|       }, |             }, | ||||||
|     }); |         }) | ||||||
|     if (response.status !== 304) { |         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 SimpleMetaTaggers, { SimpleMetaTagger } from "./SimpleMetaTagger" | ||||||
| import {ExtraFuncParams, ExtraFunctions} from "./ExtraFunctions"; | import { ExtraFuncParams, ExtraFunctions } from "./ExtraFunctions" | ||||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||||
| import {ElementStorage} from "./ElementStorage"; | import { ElementStorage } from "./ElementStorage" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... |  * 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 |  * All metatags start with an underscore | ||||||
|  */ |  */ | ||||||
| export default class MetaTagging { | 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)[]>() |     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 |      * Returns true if at least one feature has changed properties | ||||||
|      */ |      */ | ||||||
|     public static addMetatags(features: { feature: any; freshness: Date }[], |     public static addMetatags( | ||||||
|                               params: ExtraFuncParams, |         features: { feature: any; freshness: Date }[], | ||||||
|                               layer: LayerConfig, |         params: ExtraFuncParams, | ||||||
|                               state?: { allElements?: ElementStorage }, |         layer: LayerConfig, | ||||||
|                               options?: { |         state?: { allElements?: ElementStorage }, | ||||||
|                                   includeDates?: true | boolean, |         options?: { | ||||||
|                                   includeNonDates?: true | boolean, |             includeDates?: true | boolean | ||||||
|                                   evaluateStrict?: false | boolean |             includeNonDates?: true | boolean | ||||||
|                               }): boolean { |             evaluateStrict?: false | boolean | ||||||
|  |         } | ||||||
|  |     ): boolean { | ||||||
|         if (features === undefined || features.length === 0) { |         if (features === undefined || features.length === 0) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         console.log("Recalculating metatags...") |         console.log("Recalculating metatags...") | ||||||
|  | @ -52,51 +51,62 @@ export default class MetaTagging { | ||||||
|         // The calculated functions - per layer - which add the new keys
 |         // The calculated functions - per layer - which add the new keys
 | ||||||
|         const layerFuncs = this.createRetaggingFunc(layer, state) |         const layerFuncs = this.createRetaggingFunc(layer, state) | ||||||
| 
 | 
 | ||||||
|         let atLeastOneFeatureChanged = false; |         let atLeastOneFeatureChanged = false | ||||||
| 
 | 
 | ||||||
|         for (let i = 0; i < features.length; i++) { |         for (let i = 0; i < features.length; i++) { | ||||||
|             const ff = features[i]; |             const ff = features[i] | ||||||
|             const feature = ff.feature |             const feature = ff.feature | ||||||
|             const freshness = ff.freshness |             const freshness = ff.freshness | ||||||
|             let somethingChanged = false |             let somethingChanged = false | ||||||
|             let definedTags = new Set(Object.getOwnPropertyNames(feature.properties)) |             let definedTags = new Set(Object.getOwnPropertyNames(feature.properties)) | ||||||
|             for (const metatag of metatagsToApply) { |             for (const metatag of metatagsToApply) { | ||||||
|                 try { |                 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
 |                         // All keys are already defined, we probably already ran this one
 | ||||||
|                         continue |                         continue | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     if (metatag.isLazy) { |                     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!
 |                             // All keys are defined - lets skip!
 | ||||||
|                             continue |                             continue | ||||||
|                         } |                         } | ||||||
|                         somethingChanged = true; |                         somethingChanged = true | ||||||
|                         metatag.applyMetaTagsOnFeature(feature, freshness, layer, state) |                         metatag.applyMetaTagsOnFeature(feature, freshness, layer, state) | ||||||
|                         if(options?.evaluateStrict){ |                         if (options?.evaluateStrict) { | ||||||
|                             for (const key of metatag.keys) { |                             for (const key of metatag.keys) { | ||||||
|                                 feature.properties[key] |                                 feature.properties[key] | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|                     } else { |                     } else { | ||||||
|                         const newValueAdded = metatag.applyMetaTagsOnFeature(feature, freshness, layer, state) |                         const newValueAdded = metatag.applyMetaTagsOnFeature( | ||||||
|  |                             feature, | ||||||
|  |                             freshness, | ||||||
|  |                             layer, | ||||||
|  |                             state | ||||||
|  |                         ) | ||||||
|                         /* Note that the expression: |                         /* Note that the expression: | ||||||
|                         * `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)` |                          * `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)` | ||||||
|                         * Is WRONG |                          * Is WRONG | ||||||
|                         *  |                          * | ||||||
|                         * IF something changed is `true` due to an earlier run, it will short-circuit and _not_ evaluate the right hand of the OR,  |                          * IF something changed is `true` due to an earlier run, it will short-circuit and _not_ evaluate the right hand of the OR, | ||||||
|                         * thus not running an update! |                          * thus not running an update! | ||||||
|                         */ |                          */ | ||||||
|                         somethingChanged = newValueAdded || somethingChanged |                         somethingChanged = newValueAdded || somethingChanged | ||||||
|                     } |                     } | ||||||
|                 } catch (e) { |                 } 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) { |             if (layerFuncs !== undefined) { | ||||||
|                 let retaggingChanged = false; |                 let retaggingChanged = false | ||||||
|                 try { |                 try { | ||||||
|                     retaggingChanged = layerFuncs(params, feature) |                     retaggingChanged = layerFuncs(params, feature) | ||||||
|                 } catch (e) { |                 } catch (e) { | ||||||
|  | @ -113,42 +123,62 @@ export default class MetaTagging { | ||||||
|         return atLeastOneFeatureChanged |         return atLeastOneFeatureChanged | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] { |     private static createFunctionsForFeature( | ||||||
|         const functions: ((feature: any) => any)[] = []; |         layerId: string, | ||||||
|  |         calculatedTags: [string, string, boolean][] | ||||||
|  |     ): ((feature: any) => void)[] { | ||||||
|  |         const functions: ((feature: any) => any)[] = [] | ||||||
|         for (const entry of calculatedTags) { |         for (const entry of calculatedTags) { | ||||||
|             const key = entry[0] |             const key = entry[0] | ||||||
|             const code = entry[1]; |             const code = entry[1] | ||||||
|             const isStrict = entry[2] |             const isStrict = entry[2] | ||||||
|             if (code === undefined) { |             if (code === undefined) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const calculateAndAssign: ((feat: any) => any) = (feat) => { |             const calculateAndAssign: (feat: any) => any = (feat) => { | ||||||
|                 try { |                 try { | ||||||
|                     let result = new Function("feat", "return " + code + ";")(feat); |                     let result = new Function("feat", "return " + code + ";")(feat) | ||||||
|                     if (result === "") { |                     if (result === "") { | ||||||
|                         result === undefined |                         result === undefined | ||||||
|                     } |                     } | ||||||
|                     if (result !== undefined && typeof result !== "string") { |                     if (result !== undefined && typeof result !== "string") { | ||||||
|                         // Make sure it is a string!
 |                         // Make sure it is a string!
 | ||||||
|                         result = JSON.stringify(result); |                         result = JSON.stringify(result) | ||||||
|                     } |                     } | ||||||
|                     delete feat.properties[key] |                     delete feat.properties[key] | ||||||
|                     feat.properties[key] = result; |                     feat.properties[key] = result | ||||||
|                     return result |                     return result | ||||||
|                 } catch (e) { |                 } catch (e) { | ||||||
|                     if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { |                     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) |                         console.warn( | ||||||
|                         MetaTagging.errorPrintCount++; |                             "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) { |                         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) { |             if (isStrict) { | ||||||
|                 functions.push(calculateAndAssign) |                 functions.push(calculateAndAssign) | ||||||
|                 continue |                 continue | ||||||
|  | @ -162,15 +192,14 @@ export default class MetaTagging { | ||||||
|                     enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this
 |                     enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this
 | ||||||
|                     get: function () { |                     get: function () { | ||||||
|                         return calculateAndAssign(feature) |                         return calculateAndAssign(feature) | ||||||
|                     } |                     }, | ||||||
|                 }) |                 }) | ||||||
|                 return undefined |                 return undefined | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|             functions.push(f) |             functions.push(f) | ||||||
|         } |         } | ||||||
|         return functions; |         return functions | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -179,39 +208,37 @@ export default class MetaTagging { | ||||||
|      * @param state |      * @param state | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private static createRetaggingFunc(layer: LayerConfig, state): |     private static createRetaggingFunc( | ||||||
|         ((params: ExtraFuncParams, feature: any) => boolean) { |         layer: LayerConfig, | ||||||
| 
 |         state | ||||||
|         const calculatedTags: [string, string, boolean][] = layer.calculatedTags; |     ): (params: ExtraFuncParams, feature: any) => boolean { | ||||||
|  |         const calculatedTags: [string, string, boolean][] = layer.calculatedTags | ||||||
|         if (calculatedTags === undefined || calculatedTags.length === 0) { |         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) { |         if (functions === undefined) { | ||||||
|             functions = MetaTagging.createFunctionsForFeature(layer.id, calculatedTags) |             functions = MetaTagging.createFunctionsForFeature(layer.id, calculatedTags) | ||||||
|             MetaTagging.retaggingFuncCache.set(layer.id, functions) |             MetaTagging.retaggingFuncCache.set(layer.id, functions) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         return (params: ExtraFuncParams, feature) => { |         return (params: ExtraFuncParams, feature) => { | ||||||
|             const tags = feature.properties |             const tags = feature.properties | ||||||
|             if (tags === undefined) { |             if (tags === undefined) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             try { |             try { | ||||||
|                 ExtraFunctions.FullPatchFeature(params, feature); |                 ExtraFunctions.FullPatchFeature(params, feature) | ||||||
|                 for (const f of functions) { |                 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) { |             } catch (e) { | ||||||
|                 console.error("Invalid syntax in calculated tags or some other error: ", e) |                 console.error("Invalid syntax in calculated tags or some other error: ", e) | ||||||
|             } |             } | ||||||
|             return true; // Something changed
 |             return true // Something changed
 | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,18 +1,17 @@ | ||||||
| import {OsmNode, OsmRelation, OsmWay} from "../OsmObject"; | import { OsmNode, OsmRelation, OsmWay } from "../OsmObject" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Represents a single change to an object |  * Represents a single change to an object | ||||||
|  */ |  */ | ||||||
| export interface ChangeDescription { | export interface ChangeDescription { | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Metadata to be included in the changeset |      * Metadata to be included in the changeset | ||||||
|      */ |      */ | ||||||
|     meta: { |     meta: { | ||||||
|         /* |         /* | ||||||
|         * The theme with which this changeset was made |          * The theme with which this changeset was made | ||||||
|         */ |          */ | ||||||
|         theme: string, |         theme: string | ||||||
|         /** |         /** | ||||||
|          * The type of the change |          * 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' |          * THe motivation for the change, e.g. 'deleted because does not exist anymore' | ||||||
|          */ |          */ | ||||||
|         specialMotivation?: string, |         specialMotivation?: string | ||||||
|         /** |         /** | ||||||
|          * Added by Changes.ts |          * Added by Changes.ts | ||||||
|          */ |          */ | ||||||
|         distanceToObject?: number |         distanceToObject?: number | ||||||
|     }, |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Identifier of the object |      * Identifier of the object | ||||||
|      */ |      */ | ||||||
|     type: "node" | "way" | "relation", |     type: "node" | "way" | "relation" | ||||||
|     /** |     /** | ||||||
|      * Identifier of the object |      * Identifier of the object | ||||||
|      * Negative for new objects |      * Negative for new objects | ||||||
|      */ |      */ | ||||||
|     id: number, |     id: number | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * All changes to tags |      * 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 |      * 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: |      * A change to the geometry: | ||||||
|  | @ -51,17 +50,20 @@ export interface ChangeDescription { | ||||||
|      * 2) Change of way geometry |      * 2) Change of way geometry | ||||||
|      * 3) Change of relation members (untested) |      * 3) Change of relation members (untested) | ||||||
|      */ |      */ | ||||||
|     changes?: { |     changes?: | ||||||
|         lat: number, |         | { | ||||||
|         lon: number |               lat: number | ||||||
|     } | { |               lon: number | ||||||
|         /* Coordinates are only used for rendering. They should be LON, LAT |           } | ||||||
|         * */ |         | { | ||||||
|         coordinates: [number, number][] |               /* Coordinates are only used for rendering. They should be LON, LAT | ||||||
|         nodes: number[], |                * */ | ||||||
|     } | { |               coordinates: [number, number][] | ||||||
|         members: { type: "node" | "way" | "relation", ref: number, role: string }[] |               nodes: number[] | ||||||
|     } |           } | ||||||
|  |         | { | ||||||
|  |               members: { type: "node" | "way" | "relation"; ref: number; role: string }[] | ||||||
|  |           } | ||||||
| 
 | 
 | ||||||
|     /* |     /* | ||||||
|     Set to delete the object |     Set to delete the object | ||||||
|  | @ -70,7 +72,6 @@ export interface ChangeDescription { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class ChangeDescriptionTools { | export class ChangeDescriptionTools { | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Rewrites all the ids in a changeDescription |      * Rewrites all the ids in a changeDescription | ||||||
|      * |      * | ||||||
|  | @ -111,7 +112,7 @@ export class ChangeDescriptionTools { | ||||||
|      * const rewritten = ChangeDescriptionTools.rewriteIds(change, mapping) |      * const rewritten = ChangeDescriptionTools.rewriteIds(change, mapping) | ||||||
|      * rewritten.id // => 789
 |      * rewritten.id // => 789
 | ||||||
|      * rewritten.changes["nodes"] // => [42,43,44, 68453]
 |      * rewritten.changes["nodes"] // => [42,43,44, 68453]
 | ||||||
|      *  |      * | ||||||
|      * // should rewrite ids in relationship members
 |      * // should rewrite ids in relationship members
 | ||||||
|      * const change = <ChangeDescription> { |      * const change = <ChangeDescription> { | ||||||
|      *     type: "way", |      *     type: "way", | ||||||
|  | @ -130,44 +131,49 @@ export class ChangeDescriptionTools { | ||||||
|      * rewritten.changes["members"] // => [{type: "way", ref: 42, role: "outer"},{type: "way", ref: 48, role: "outer"}]
 |      * 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 key = change.type + "/" + change.id | ||||||
| 
 | 
 | ||||||
|         const wayHasChangedNode = ((change.changes ?? {})["nodes"] ?? []).some(id => mappings.has("node/" + id)); |         const wayHasChangedNode = ((change.changes ?? {})["nodes"] ?? []).some((id) => | ||||||
|         const relationHasChangedMembers = ((change.changes ?? {})["members"] ?? []) |             mappings.has("node/" + id) | ||||||
|             .some((obj:{type: string, ref: number}) => mappings.has(obj.type+"/" + obj.ref)); |         ) | ||||||
|  |         const relationHasChangedMembers = ((change.changes ?? {})["members"] ?? []).some( | ||||||
|  |             (obj: { type: string; ref: number }) => mappings.has(obj.type + "/" + obj.ref) | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         const hasSomeChange = mappings.has(key) |         const hasSomeChange = mappings.has(key) || wayHasChangedNode || relationHasChangedMembers | ||||||
|             || wayHasChangedNode || relationHasChangedMembers |         if (hasSomeChange) { | ||||||
|         if(hasSomeChange){ |             change = { ...change } | ||||||
|             change = {...change} |  | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         if (mappings.has(key)) { |         if (mappings.has(key)) { | ||||||
|             const [_, newId] = mappings.get(key).split("/") |             const [_, newId] = mappings.get(key).split("/") | ||||||
|             change.id = Number.parseInt(newId) |             change.id = Number.parseInt(newId) | ||||||
|         } |         } | ||||||
|         if(wayHasChangedNode){ |         if (wayHasChangedNode) { | ||||||
|             change.changes = {...change.changes} |             change.changes = { ...change.changes } | ||||||
|             change.changes["nodes"] = change.changes["nodes"].map(id => { |             change.changes["nodes"] = change.changes["nodes"].map((id) => { | ||||||
|                 const key = "node/"+id |                 const key = "node/" + id | ||||||
|                 if(!mappings.has(key)){ |                 if (!mappings.has(key)) { | ||||||
|                     return id |                     return id | ||||||
|                 } |                 } | ||||||
|                 const [_, newId] = mappings.get(key).split("/") |                 const [_, newId] = mappings.get(key).split("/") | ||||||
|                 return Number.parseInt(newId) |                 return Number.parseInt(newId) | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
|         if(relationHasChangedMembers){ |         if (relationHasChangedMembers) { | ||||||
|             change.changes = {...change.changes} |             change.changes = { ...change.changes } | ||||||
|             change.changes["members"] = change.changes["members"].map( |             change.changes["members"] = change.changes["members"].map( | ||||||
|                 (obj:{type: string, ref: number}) => { |                 (obj: { type: string; ref: number }) => { | ||||||
|                     const key = obj.type+"/"+obj.ref; |                     const key = obj.type + "/" + obj.ref | ||||||
|                     if(!mappings.has(key)){ |                     if (!mappings.has(key)) { | ||||||
|                         return obj |                         return obj | ||||||
|                     } |                     } | ||||||
|                     const [_, newId] = mappings.get(key).split("/") |                     const [_, newId] = mappings.get(key).split("/") | ||||||
|                     return {...obj, ref: Number.parseInt(newId)} |                     return { ...obj, ref: Number.parseInt(newId) } | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|  | @ -193,4 +199,4 @@ export class ChangeDescriptionTools { | ||||||
|                 return r.asGeoJson().geometry |                 return r.asGeoJson().geometry | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,41 +1,44 @@ | ||||||
| import {ChangeDescription} from "./ChangeDescription"; | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import OsmChangeAction from "./OsmChangeAction"; | import OsmChangeAction from "./OsmChangeAction" | ||||||
| import {Changes} from "../Changes"; | import { Changes } from "../Changes" | ||||||
| 
 | 
 | ||||||
| export default class ChangeLocationAction extends OsmChangeAction { | export default class ChangeLocationAction extends OsmChangeAction { | ||||||
|     private readonly _id: number; |     private readonly _id: number | ||||||
|     private readonly _newLonLat: [number, number]; |     private readonly _newLonLat: [number, number] | ||||||
|     private readonly _meta: { theme: string; reason: string }; |     private readonly _meta: { theme: string; reason: string } | ||||||
| 
 | 
 | ||||||
|     constructor(id: string, newLonLat: [number, number], meta: { |     constructor( | ||||||
|         theme: string, |         id: string, | ||||||
|         reason: string |         newLonLat: [number, number], | ||||||
|     }) { |         meta: { | ||||||
|         super(id, true); |             theme: string | ||||||
|  |             reason: string | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         super(id, true) | ||||||
|         if (!id.startsWith("node/")) { |         if (!id.startsWith("node/")) { | ||||||
|             throw "Invalid ID: only 'node/number' is accepted" |             throw "Invalid ID: only 'node/number' is accepted" | ||||||
|         } |         } | ||||||
|         this._id = Number(id.substring("node/".length)) |         this._id = Number(id.substring("node/".length)) | ||||||
|         this._newLonLat = newLonLat; |         this._newLonLat = newLonLat | ||||||
|         this._meta = meta; |         this._meta = meta | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
| 
 |  | ||||||
|         const d: ChangeDescription = { |         const d: ChangeDescription = { | ||||||
|             changes: { |             changes: { | ||||||
|                 lat: this._newLonLat[1], |                 lat: this._newLonLat[1], | ||||||
|                 lon: this._newLonLat[0] |                 lon: this._newLonLat[0], | ||||||
|             }, |             }, | ||||||
|             type: "node", |             type: "node", | ||||||
|             id: this._id, meta: { |             id: this._id, | ||||||
|  |             meta: { | ||||||
|                 changeType: "move", |                 changeType: "move", | ||||||
|                 theme: this._meta.theme, |                 theme: this._meta.theme, | ||||||
|                 specialMotivation: this._meta.reason |                 specialMotivation: this._meta.reason, | ||||||
|             } |             }, | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return [d] |         return [d] | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,65 +1,77 @@ | ||||||
| import OsmChangeAction from "./OsmChangeAction"; | import OsmChangeAction from "./OsmChangeAction" | ||||||
| import {Changes} from "../Changes"; | import { Changes } from "../Changes" | ||||||
| import {ChangeDescription} from "./ChangeDescription"; | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | import { TagsFilter } from "../../Tags/TagsFilter" | ||||||
| import {OsmTags} from "../../../Models/OsmFeature"; | import { OsmTags } from "../../../Models/OsmFeature" | ||||||
| 
 | 
 | ||||||
| export default class ChangeTagAction extends OsmChangeAction { | export default class ChangeTagAction extends OsmChangeAction { | ||||||
|     private readonly _elementId: string; |     private readonly _elementId: string | ||||||
|     private readonly _tagsFilter: TagsFilter; |     private readonly _tagsFilter: TagsFilter | ||||||
|     private readonly _currentTags: Record<string, string> | OsmTags; |     private readonly _currentTags: Record<string, string> | OsmTags | ||||||
|     private readonly _meta: { theme: string, changeType: string }; |     private readonly _meta: { theme: string; changeType: string } | ||||||
| 
 | 
 | ||||||
|     constructor(elementId: string,  |     constructor( | ||||||
|                 tagsFilter: TagsFilter,  |         elementId: string, | ||||||
|                 currentTags: Record<string, string>, meta: { |         tagsFilter: TagsFilter, | ||||||
|         theme: string, |         currentTags: Record<string, string>, | ||||||
|         changeType: "answer" | "soft-delete" | "add-image" | string |         meta: { | ||||||
|     }) { |             theme: string | ||||||
|         super(elementId, true); |             changeType: "answer" | "soft-delete" | "add-image" | string | ||||||
|         this._elementId = elementId; |         } | ||||||
|         this._tagsFilter = tagsFilter; |     ) { | ||||||
|         this._currentTags = currentTags; |         super(elementId, true) | ||||||
|         this._meta = meta; |         this._elementId = elementId | ||||||
|  |         this._tagsFilter = tagsFilter | ||||||
|  |         this._currentTags = currentTags | ||||||
|  |         this._meta = meta | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Doublechecks that no stupid values are added |      * Doublechecks that no stupid values are added | ||||||
|      */ |      */ | ||||||
|     private static checkChange(kv: { k: string, v: string }): { k: string, v: string } { |     private static checkChange(kv: { k: string; v: string }): { k: string; v: string } { | ||||||
|         const key = kv.k; |         const key = kv.k | ||||||
|         const value = kv.v; |         const value = kv.v | ||||||
|         if (key === undefined || key === null) { |         if (key === undefined || key === null) { | ||||||
|             console.error("Invalid key:", key); |             console.error("Invalid key:", key) | ||||||
|             return undefined; |             return undefined | ||||||
|         } |         } | ||||||
|         if (value === undefined || value === null) { |         if (value === undefined || value === null) { | ||||||
|             console.error("Invalid value for ", key, ":", value); |             console.error("Invalid value for ", key, ":", value) | ||||||
|             return undefined; |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (typeof value !== "string") { |         if (typeof value !== "string") { | ||||||
|             console.error("Invalid value for ", key, "as it is not a string:", value) |             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") |             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[]> { |     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 typeId = this._elementId.split("/") | ||||||
|         const type = typeId[0] |         const type = typeId[0] | ||||||
|         const id = Number(typeId  [1]) |         const id = Number(typeId[1]) | ||||||
|         return [{ |         return [ | ||||||
|             type: <"node" | "way" | "relation">type, |             { | ||||||
|             id: id, |                 type: <"node" | "way" | "relation">type, | ||||||
|             tags: changedTags, |                 id: id, | ||||||
|             meta: this._meta |                 tags: changedTags, | ||||||
|         }] |                 meta: this._meta, | ||||||
|  |             }, | ||||||
|  |         ] | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,64 +1,69 @@ | ||||||
| import {OsmCreateAction} from "./OsmChangeAction"; | import { OsmCreateAction } from "./OsmChangeAction" | ||||||
| import {Tag} from "../../Tags/Tag"; | import { Tag } from "../../Tags/Tag" | ||||||
| import {Changes} from "../Changes"; | import { Changes } from "../Changes" | ||||||
| import {ChangeDescription} from "./ChangeDescription"; | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import FeaturePipelineState from "../../State/FeaturePipelineState"; | import FeaturePipelineState from "../../State/FeaturePipelineState" | ||||||
| import FeatureSource from "../../FeatureSource/FeatureSource"; | import FeatureSource from "../../FeatureSource/FeatureSource" | ||||||
| import CreateNewWayAction from "./CreateNewWayAction"; | import CreateNewWayAction from "./CreateNewWayAction" | ||||||
| import CreateWayWithPointReuseAction, {MergePointConfig} from "./CreateWayWithPointReuseAction"; | import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWithPointReuseAction" | ||||||
| import {And} from "../../Tags/And"; | import { And } from "../../Tags/And" | ||||||
| import {TagUtils} from "../../Tags/TagUtils"; | import { TagUtils } from "../../Tags/TagUtils" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points |  * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points | ||||||
|  */ |  */ | ||||||
| export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAction { | export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAction { | ||||||
|     public newElementId: string = undefined; |     public newElementId: string = undefined | ||||||
|     public newElementIdNumber: number = undefined; |     public newElementIdNumber: number = undefined | ||||||
|     private readonly _tags: Tag[]; |     private readonly _tags: Tag[] | ||||||
|     private readonly createOuterWay: CreateWayWithPointReuseAction |     private readonly createOuterWay: CreateWayWithPointReuseAction | ||||||
|     private readonly createInnerWays: CreateNewWayAction[] |     private readonly createInnerWays: CreateNewWayAction[] | ||||||
|     private readonly geojsonPreview: any; |     private readonly geojsonPreview: any | ||||||
|     private readonly theme: string; |     private readonly theme: string | ||||||
|     private readonly changeType: "import" | "create" | string; |     private readonly changeType: "import" | "create" | string | ||||||
| 
 | 
 | ||||||
|     constructor(tags: Tag[], |     constructor( | ||||||
|                 outerRingCoordinates: [number, number][], |         tags: Tag[], | ||||||
|                 innerRingsCoordinates: [number, number][][], |         outerRingCoordinates: [number, number][], | ||||||
|                 state: FeaturePipelineState, |         innerRingsCoordinates: [number, number][][], | ||||||
|                 config: MergePointConfig[], |         state: FeaturePipelineState, | ||||||
|                 changeType: "import" | "create" | string |         config: MergePointConfig[], | ||||||
|  |         changeType: "import" | "create" | string | ||||||
|     ) { |     ) { | ||||||
|         super(null, true); |         super(null, true) | ||||||
|         this._tags = [...tags, new Tag("type", "multipolygon")]; |         this._tags = [...tags, new Tag("type", "multipolygon")] | ||||||
|         this.changeType = changeType; |         this.changeType = changeType | ||||||
|         this.theme = state?.layoutToUse?.id ?? "" |         this.theme = state?.layoutToUse?.id ?? "" | ||||||
|         this.createOuterWay = new CreateWayWithPointReuseAction([], outerRingCoordinates, state, config) |         this.createOuterWay = new CreateWayWithPointReuseAction( | ||||||
|         this.createInnerWays = innerRingsCoordinates.map(ringCoordinates => |             [], | ||||||
|             new CreateNewWayAction([], |             outerRingCoordinates, | ||||||
|                 ringCoordinates.map(([lon, lat]) => ({lat, lon})), |             state, | ||||||
|                 {theme: state?.layoutToUse?.id})) |             config | ||||||
|  |         ) | ||||||
|  |         this.createInnerWays = innerRingsCoordinates.map( | ||||||
|  |             (ringCoordinates) => | ||||||
|  |                 new CreateNewWayAction( | ||||||
|  |                     [], | ||||||
|  |                     ringCoordinates.map(([lon, lat]) => ({ lat, lon })), | ||||||
|  |                     { theme: state?.layoutToUse?.id } | ||||||
|  |                 ) | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         this.geojsonPreview = { |         this.geojsonPreview = { | ||||||
|             type: "Feature", |             type: "Feature", | ||||||
|             properties: TagUtils.changeAsProperties(new And(this._tags).asChange({})), |             properties: TagUtils.changeAsProperties(new And(this._tags).asChange({})), | ||||||
|             geometry: { |             geometry: { | ||||||
|                 type: "Polygon", |                 type: "Polygon", | ||||||
|                 coordinates: [ |                 coordinates: [outerRingCoordinates, ...innerRingsCoordinates], | ||||||
|                     outerRingCoordinates, |             }, | ||||||
|                     ...innerRingsCoordinates |  | ||||||
|                 ] |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async getPreview(): Promise<FeatureSource> { |     public async getPreview(): Promise<FeatureSource> { | ||||||
|         const outerPreview = await this.createOuterWay.getPreview() |         const outerPreview = await this.createOuterWay.getPreview() | ||||||
|         outerPreview.features.data.push({ |         outerPreview.features.data.push({ | ||||||
|             freshness: new Date(), |             freshness: new Date(), | ||||||
|             feature: this.geojsonPreview |             feature: this.geojsonPreview, | ||||||
|         }) |         }) | ||||||
|         return outerPreview |         return outerPreview | ||||||
|     } |     } | ||||||
|  | @ -66,13 +71,12 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct | ||||||
|     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
|         console.log("Running CMPWPRA") |         console.log("Running CMPWPRA") | ||||||
|         const descriptions: ChangeDescription[] = [] |         const descriptions: ChangeDescription[] = [] | ||||||
|         descriptions.push(...await this.createOuterWay.CreateChangeDescriptions(changes)); |         descriptions.push(...(await this.createOuterWay.CreateChangeDescriptions(changes))) | ||||||
|         for (const innerWay of this.createInnerWays) { |         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 |         this.newElementId = "relation/" + this.newElementIdNumber | ||||||
|         descriptions.push({ |         descriptions.push({ | ||||||
|             type: "relation", |             type: "relation", | ||||||
|  | @ -80,24 +84,25 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct | ||||||
|             tags: new And(this._tags).asChange({}), |             tags: new And(this._tags).asChange({}), | ||||||
|             meta: { |             meta: { | ||||||
|                 theme: this.theme, |                 theme: this.theme, | ||||||
|                 changeType: this.changeType |                 changeType: this.changeType, | ||||||
|             }, |             }, | ||||||
|             changes: { |             changes: { | ||||||
|                 members: [ |                 members: [ | ||||||
|                     { |                     { | ||||||
|                         type: "way", |                         type: "way", | ||||||
|                         ref: this.createOuterWay.newElementIdNumber, |                         ref: this.createOuterWay.newElementIdNumber, | ||||||
|                         role: "outer" |                         role: "outer", | ||||||
|                     }, |                     }, | ||||||
|                     // @ts-ignore
 |                     // @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 |         return descriptions | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,13 +1,12 @@ | ||||||
| import {Tag} from "../../Tags/Tag"; | import { Tag } from "../../Tags/Tag" | ||||||
| import {OsmCreateAction} from "./OsmChangeAction"; | import { OsmCreateAction } from "./OsmChangeAction" | ||||||
| import {Changes} from "../Changes"; | import { Changes } from "../Changes" | ||||||
| import {ChangeDescription} from "./ChangeDescription"; | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import {And} from "../../Tags/And"; | import { And } from "../../Tags/And" | ||||||
| import {OsmWay} from "../OsmObject"; | import { OsmWay } from "../OsmObject" | ||||||
| import {GeoOperations} from "../../GeoOperations"; | import { GeoOperations } from "../../GeoOperations" | ||||||
| 
 | 
 | ||||||
| export default class CreateNewNodeAction extends OsmCreateAction { | export default class CreateNewNodeAction extends OsmCreateAction { | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Maps previously created points onto their assigned ID, to reuse the point if uplaoded |      * Maps previously created points onto their assigned ID, to reuse the point if uplaoded | ||||||
|      * "lat,lon" --> id |      * "lat,lon" --> id | ||||||
|  | @ -15,46 +14,47 @@ export default class CreateNewNodeAction extends OsmCreateAction { | ||||||
|     private static readonly previouslyCreatedPoints = new Map<string, number>() |     private static readonly previouslyCreatedPoints = new Map<string, number>() | ||||||
|     public newElementId: string = undefined |     public newElementId: string = undefined | ||||||
|     public newElementIdNumber: number = undefined |     public newElementIdNumber: number = undefined | ||||||
|     private readonly _basicTags: Tag[]; |     private readonly _basicTags: Tag[] | ||||||
|     private readonly _lat: number; |     private readonly _lat: number | ||||||
|     private readonly _lon: number; |     private readonly _lon: number | ||||||
|     private readonly _snapOnto: OsmWay; |     private readonly _snapOnto: OsmWay | ||||||
|     private readonly _reusePointDistance: number; |     private readonly _reusePointDistance: number | ||||||
|     private meta: { changeType: "create" | "import"; theme: string; specialMotivation?: string }; |     private meta: { changeType: "create" | "import"; theme: string; specialMotivation?: string } | ||||||
|     private readonly _reusePreviouslyCreatedPoint: boolean; |     private readonly _reusePreviouslyCreatedPoint: boolean | ||||||
| 
 | 
 | ||||||
|      |     constructor( | ||||||
|     constructor(basicTags: Tag[], |         basicTags: Tag[], | ||||||
|                 lat: number, lon: number, |         lat: number, | ||||||
|                 options: { |         lon: number, | ||||||
|                     allowReuseOfPreviouslyCreatedPoints?: boolean, |         options: { | ||||||
|                     snapOnto?: OsmWay, |             allowReuseOfPreviouslyCreatedPoints?: boolean | ||||||
|                     reusePointWithinMeters?: number, |             snapOnto?: OsmWay | ||||||
|                     theme: string, |             reusePointWithinMeters?: number | ||||||
|                     changeType: "create" | "import" | null, |             theme: string | ||||||
|                     specialMotivation?: string |             changeType: "create" | "import" | null | ||||||
|                 }) { |             specialMotivation?: string | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|         super(null, basicTags !== undefined && basicTags.length > 0) |         super(null, basicTags !== undefined && basicTags.length > 0) | ||||||
|         this._basicTags = basicTags; |         this._basicTags = basicTags | ||||||
|         this._lat = lat; |         this._lat = lat | ||||||
|         this._lon = lon; |         this._lon = lon | ||||||
|         if (lat === undefined || lon === undefined) { |         if (lat === undefined || lon === undefined) { | ||||||
|             throw "Lat or lon are undefined!" |             throw "Lat or lon are undefined!" | ||||||
|         } |         } | ||||||
|         this._snapOnto = options?.snapOnto; |         this._snapOnto = options?.snapOnto | ||||||
|         this._reusePointDistance = options?.reusePointWithinMeters ?? 1 |         this._reusePointDistance = options?.reusePointWithinMeters ?? 1 | ||||||
|         this._reusePreviouslyCreatedPoint = options?.allowReuseOfPreviouslyCreatedPoints ?? (basicTags.length === 0) |         this._reusePreviouslyCreatedPoint = | ||||||
|  |             options?.allowReuseOfPreviouslyCreatedPoints ?? basicTags.length === 0 | ||||||
|         this.meta = { |         this.meta = { | ||||||
|             theme: options.theme, |             theme: options.theme, | ||||||
|             changeType: options.changeType, |             changeType: options.changeType, | ||||||
|             specialMotivation: options.specialMotivation |             specialMotivation: options.specialMotivation, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
| 
 |  | ||||||
|         if (this._reusePreviouslyCreatedPoint) { |         if (this._reusePreviouslyCreatedPoint) { | ||||||
| 
 |  | ||||||
|             const key = this._lat + "," + this._lon |             const key = this._lat + "," + this._lon | ||||||
|             const prev = CreateNewNodeAction.previouslyCreatedPoints |             const prev = CreateNewNodeAction.previouslyCreatedPoints | ||||||
|             if (prev.has(key)) { |             if (prev.has(key)) { | ||||||
|  | @ -64,17 +64,23 @@ export default class CreateNewNodeAction extends OsmCreateAction { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         const id = changes.getNewID() |         const id = changes.getNewID() | ||||||
|         const properties = { |         const properties = { | ||||||
|             id: "node/" + id |             id: "node/" + id, | ||||||
|         } |         } | ||||||
|         this.setElementId(id) |         this.setElementId(id) | ||||||
|         for (const kv of this._basicTags) { |         for (const kv of this._basicTags) { | ||||||
|             if (typeof kv.value !== "string") { |             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 = { |         const newPointChange: ChangeDescription = { | ||||||
|  | @ -83,32 +89,31 @@ export default class CreateNewNodeAction extends OsmCreateAction { | ||||||
|             id: id, |             id: id, | ||||||
|             changes: { |             changes: { | ||||||
|                 lat: this._lat, |                 lat: this._lat, | ||||||
|                 lon: this._lon |                 lon: this._lon, | ||||||
|             }, |             }, | ||||||
|             meta: this.meta |             meta: this.meta, | ||||||
|         } |         } | ||||||
|         if (this._snapOnto === undefined) { |         if (this._snapOnto === undefined) { | ||||||
|             return [newPointChange] |             return [newPointChange] | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         // Project the point onto the way
 |         // Project the point onto the way
 | ||||||
|         console.log("Snapping a node onto an existing way...") |         console.log("Snapping a node onto an existing way...") | ||||||
|         const geojson = this._snapOnto.asGeoJson() |         const geojson = this._snapOnto.asGeoJson() | ||||||
|         const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat]) |         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 |         const index = projected.properties.index | ||||||
|         // We check that it isn't close to an already existing point
 |         // We check that it isn't close to an already existing point
 | ||||||
|         let reusedPointId = undefined; |         let reusedPointId = undefined | ||||||
|         let outerring : [number,number][]; |         let outerring: [number, number][] | ||||||
|          | 
 | ||||||
|         if(geojson.geometry.type === "LineString"){ |         if (geojson.geometry.type === "LineString") { | ||||||
|            outerring = <[number, number][]> geojson.geometry.coordinates |             outerring = <[number, number][]>geojson.geometry.coordinates | ||||||
|         }else if(geojson.geometry.type === "Polygon"){ |         } else if (geojson.geometry.type === "Polygon") { | ||||||
|            outerring =<[number, number][]>  geojson.geometry.coordinates[0] |             outerring = <[number, number][]>geojson.geometry.coordinates[0] | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         const prev= outerring[index] |         const prev = outerring[index] | ||||||
|         if (GeoOperations.distanceBetween(prev, projectedCoor) < this._reusePointDistance) { |         if (GeoOperations.distanceBetween(prev, projectedCoor) < this._reusePointDistance) { | ||||||
|             // We reuse this point instead!
 |             // We reuse this point instead!
 | ||||||
|             reusedPointId = this._snapOnto.nodes[index] |             reusedPointId = this._snapOnto.nodes[index] | ||||||
|  | @ -120,20 +125,24 @@ export default class CreateNewNodeAction extends OsmCreateAction { | ||||||
|         } |         } | ||||||
|         if (reusedPointId !== undefined) { |         if (reusedPointId !== undefined) { | ||||||
|             this.setElementId(reusedPointId) |             this.setElementId(reusedPointId) | ||||||
|             return [{ |             return [ | ||||||
|                 tags: new And(this._basicTags).asChange(properties), |                 { | ||||||
|                 type: "node", |                     tags: new And(this._basicTags).asChange(properties), | ||||||
|                 id: reusedPointId, |                     type: "node", | ||||||
|                 meta: this.meta |                     id: reusedPointId, | ||||||
|             }] |                     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] |         const ids = [...this._snapOnto.nodes] | ||||||
| 
 | 
 | ||||||
|         locations.splice(index + 1, 0, [this._lon, this._lat]) |         locations.splice(index + 1, 0, [this._lon, this._lat]) | ||||||
|         ids.splice(index + 1, 0, id) |         ids.splice(index + 1, 0, id) | ||||||
|          | 
 | ||||||
|         // Allright, we have to insert a new point in the way
 |         // Allright, we have to insert a new point in the way
 | ||||||
|         return [ |         return [ | ||||||
|             newPointChange, |             newPointChange, | ||||||
|  | @ -142,15 +151,15 @@ export default class CreateNewNodeAction extends OsmCreateAction { | ||||||
|                 id: this._snapOnto.id, |                 id: this._snapOnto.id, | ||||||
|                 changes: { |                 changes: { | ||||||
|                     coordinates: locations, |                     coordinates: locations, | ||||||
|                     nodes: ids |                     nodes: ids, | ||||||
|                 }, |                 }, | ||||||
|                 meta: this.meta |                 meta: this.meta, | ||||||
|             } |             }, | ||||||
|         ] |         ] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private setElementId(id: number) { |     private setElementId(id: number) { | ||||||
|         this.newElementIdNumber = id; |         this.newElementIdNumber = id | ||||||
|         this.newElementId = "node/" + id |         this.newElementId = "node/" + id | ||||||
|         if (!this._reusePreviouslyCreatedPoint) { |         if (!this._reusePreviouslyCreatedPoint) { | ||||||
|             return |             return | ||||||
|  | @ -158,6 +167,4 @@ export default class CreateNewNodeAction extends OsmCreateAction { | ||||||
|         const key = this._lat + "," + this._lon |         const key = this._lat + "," + this._lon | ||||||
|         CreateNewNodeAction.previouslyCreatedPoints.set(key, id) |         CreateNewNodeAction.previouslyCreatedPoints.set(key, id) | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,19 +1,18 @@ | ||||||
| import {ChangeDescription} from "./ChangeDescription"; | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import {OsmCreateAction} from "./OsmChangeAction"; | import { OsmCreateAction } from "./OsmChangeAction" | ||||||
| import {Changes} from "../Changes"; | import { Changes } from "../Changes" | ||||||
| import {Tag} from "../../Tags/Tag"; | import { Tag } from "../../Tags/Tag" | ||||||
| import CreateNewNodeAction from "./CreateNewNodeAction"; | import CreateNewNodeAction from "./CreateNewNodeAction" | ||||||
| import {And} from "../../Tags/And"; | import { And } from "../../Tags/And" | ||||||
| 
 | 
 | ||||||
| export default class CreateNewWayAction extends OsmCreateAction { | export default class CreateNewWayAction extends OsmCreateAction { | ||||||
|     public newElementId: string = undefined |     public newElementId: string = undefined | ||||||
|     public newElementIdNumber: number = undefined; |     public newElementIdNumber: number = undefined | ||||||
|     private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[]; |     private readonly coordinates: { nodeId?: number; lat: number; lon: number }[] | ||||||
|     private readonly tags: Tag[]; |     private readonly tags: Tag[] | ||||||
|     private readonly _options: { |     private readonly _options: { | ||||||
|         theme: string |         theme: string | ||||||
|     }; |     } | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|     /*** |     /*** | ||||||
|      * Creates a new way to upload to OSM |      * 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 coordinates: the coordinates. Might have a nodeId, in this case, this node will be used | ||||||
|      * @param options |      * @param options | ||||||
|      */ |      */ | ||||||
|     constructor(tags: Tag[], coordinates: ({ nodeId?: number, lat: number, lon: number })[], |     constructor( | ||||||
|                 options: { |         tags: Tag[], | ||||||
|                     theme: string |         coordinates: { nodeId?: number; lat: number; lon: number }[], | ||||||
|                 }) { |         options: { | ||||||
|  |             theme: string | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|         super(null, true) |         super(null, true) | ||||||
|         this.coordinates = []; |         this.coordinates = [] | ||||||
| 
 | 
 | ||||||
|         for (const coordinate of coordinates) { |         for (const coordinate of coordinates) { | ||||||
|             /* The 'PointReuseAction' is a bit buggy and might generate duplicate ids. |             /* The 'PointReuseAction' is a bit buggy and might generate duplicate ids. | ||||||
|                 We filter those here, as the CreateWayWithPointReuseAction delegates the actual creation to here. |                 We filter those here, as the CreateWayWithPointReuseAction delegates the actual creation to here. | ||||||
|                 Filtering here also prevents similar bugs in other actions |                 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
 |                 // 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 |                 continue | ||||||
|             } |             } | ||||||
|              | 
 | ||||||
|             this.coordinates.push(coordinate) |             this.coordinates.push(coordinate) | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         this.tags = tags; |         this.tags = tags | ||||||
|         this._options = options; |         this._options = options | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
| 
 |  | ||||||
|         const newElements: ChangeDescription[] = [] |         const newElements: ChangeDescription[] = [] | ||||||
| 
 | 
 | ||||||
|         const pointIds: number[] = [] |         const pointIds: number[] = [] | ||||||
|  | @ -60,16 +70,15 @@ export default class CreateNewWayAction extends OsmCreateAction { | ||||||
|             const newPoint = new CreateNewNodeAction([], coordinate.lat, coordinate.lon, { |             const newPoint = new CreateNewNodeAction([], coordinate.lat, coordinate.lon, { | ||||||
|                 allowReuseOfPreviouslyCreatedPoints: true, |                 allowReuseOfPreviouslyCreatedPoints: true, | ||||||
|                 changeType: null, |                 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) |             pointIds.push(newPoint.newElementIdNumber) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // We have all created (or reused) all the points!
 |         // We have all created (or reused) all the points!
 | ||||||
|         // Time to create the actual way
 |         // Time to create the actual way
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         const id = changes.getNewID() |         const id = changes.getNewID() | ||||||
|         this.newElementIdNumber = id |         this.newElementIdNumber = id | ||||||
|         const newWay = <ChangeDescription>{ |         const newWay = <ChangeDescription>{ | ||||||
|  | @ -77,18 +86,16 @@ export default class CreateNewWayAction extends OsmCreateAction { | ||||||
|             type: "way", |             type: "way", | ||||||
|             meta: { |             meta: { | ||||||
|                 theme: this._options.theme, |                 theme: this._options.theme, | ||||||
|                 changeType: "import" |                 changeType: "import", | ||||||
|             }, |             }, | ||||||
|             tags: new And(this.tags).asChange({}), |             tags: new And(this.tags).asChange({}), | ||||||
|             changes: { |             changes: { | ||||||
|                 nodes: pointIds, |                 nodes: pointIds, | ||||||
|                 coordinates: this.coordinates.map(c => [c.lon, c.lat]) |                 coordinates: this.coordinates.map((c) => [c.lon, c.lat]), | ||||||
|             } |             }, | ||||||
|         } |         } | ||||||
|         newElements.push(newWay) |         newElements.push(newWay) | ||||||
|         this.newElementId = "way/" + id |         this.newElementId = "way/" + id | ||||||
|         return newElements |         return newElements | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,20 +1,19 @@ | ||||||
| import {OsmCreateAction} from "./OsmChangeAction"; | import { OsmCreateAction } from "./OsmChangeAction" | ||||||
| import {Tag} from "../../Tags/Tag"; | import { Tag } from "../../Tags/Tag" | ||||||
| import {Changes} from "../Changes"; | import { Changes } from "../Changes" | ||||||
| import {ChangeDescription} from "./ChangeDescription"; | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import FeaturePipelineState from "../../State/FeaturePipelineState"; | import FeaturePipelineState from "../../State/FeaturePipelineState" | ||||||
| import {BBox} from "../../BBox"; | import { BBox } from "../../BBox" | ||||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | import { TagsFilter } from "../../Tags/TagsFilter" | ||||||
| import {GeoOperations} from "../../GeoOperations"; | import { GeoOperations } from "../../GeoOperations" | ||||||
| import FeatureSource from "../../FeatureSource/FeatureSource"; | import FeatureSource from "../../FeatureSource/FeatureSource" | ||||||
| import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"; | import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" | ||||||
| import CreateNewNodeAction from "./CreateNewNodeAction"; | import CreateNewNodeAction from "./CreateNewNodeAction" | ||||||
| import CreateNewWayAction from "./CreateNewWayAction"; | import CreateNewWayAction from "./CreateNewWayAction" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| export interface MergePointConfig { | export interface MergePointConfig { | ||||||
|     withinRangeOfM: number, |     withinRangeOfM: number | ||||||
|     ifMatches: TagsFilter, |     ifMatches: TagsFilter | ||||||
|     mode: "reuse_osm_point" | "move_osm_point" |     mode: "reuse_osm_point" | "move_osm_point" | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -33,12 +32,12 @@ interface CoordinateInfo { | ||||||
|     /** |     /** | ||||||
|      * The new coordinate |      * 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. |      * 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 |      * 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 |      * 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 |          * Distance in meters between the target coordinate and this candidate coordinate | ||||||
|          */ |          */ | ||||||
|         d: number, |         d: number | ||||||
|         node: any, |         node: any | ||||||
|         config: MergePointConfig |         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 |  * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points | ||||||
|  */ |  */ | ||||||
| export default class CreateWayWithPointReuseAction extends OsmCreateAction { | export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|     public newElementId: string = undefined; |     public newElementId: string = undefined | ||||||
|     public newElementIdNumber: number = undefined |     public newElementIdNumber: number = undefined | ||||||
|     private readonly _tags: Tag[]; |     private readonly _tags: Tag[] | ||||||
|     /** |     /** | ||||||
|      * lngLat-coordinates |      * lngLat-coordinates | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private _coordinateInfo: CoordinateInfo[]; |     private _coordinateInfo: CoordinateInfo[] | ||||||
|     private _state: FeaturePipelineState; |     private _state: FeaturePipelineState | ||||||
|     private _config: MergePointConfig[]; |     private _config: MergePointConfig[] | ||||||
| 
 | 
 | ||||||
|     constructor(tags: Tag[], |     constructor( | ||||||
|                 coordinates: [number, number][], |         tags: Tag[], | ||||||
|                 state: FeaturePipelineState, |         coordinates: [number, number][], | ||||||
|                 config: MergePointConfig[] |         state: FeaturePipelineState, | ||||||
|  |         config: MergePointConfig[] | ||||||
|     ) { |     ) { | ||||||
|         super(null, true); |         super(null, true) | ||||||
|         this._tags = tags; |         this._tags = tags | ||||||
|         this._state = state; |         this._state = state | ||||||
|         this._config = config; |         this._config = config | ||||||
| 
 | 
 | ||||||
|         // The main logic of this class: the coordinateInfo contains all the changes
 |         // 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> { |     public async getPreview(): Promise<FeatureSource> { | ||||||
| 
 |  | ||||||
|         const features = [] |         const features = [] | ||||||
|         let geometryMoved = false; |         let geometryMoved = false | ||||||
|         for (let i = 0; i < this._coordinateInfo.length; i++) { |         for (let i = 0; i < this._coordinateInfo.length; i++) { | ||||||
|             const coordinateInfo = this._coordinateInfo[i]; |             const coordinateInfo = this._coordinateInfo[i] | ||||||
|             if (coordinateInfo.identicalTo !== undefined) { |             if (coordinateInfo.identicalTo !== undefined) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             if (coordinateInfo.closebyNodes === undefined || coordinateInfo.closebyNodes.length === 0) { |             if ( | ||||||
| 
 |                 coordinateInfo.closebyNodes === undefined || | ||||||
|  |                 coordinateInfo.closebyNodes.length === 0 | ||||||
|  |             ) { | ||||||
|                 const newPoint = { |                 const newPoint = { | ||||||
|                     type: "Feature", |                     type: "Feature", | ||||||
|                     properties: { |                     properties: { | ||||||
|                         "newpoint": "yes", |                         newpoint: "yes", | ||||||
|                         id: "new-geometry-with-reuse-" + i |                         id: "new-geometry-with-reuse-" + i, | ||||||
|                     }, |                     }, | ||||||
|                     geometry: { |                     geometry: { | ||||||
|                         type: "Point", |                         type: "Point", | ||||||
|                         coordinates: coordinateInfo.lngLat |                         coordinates: coordinateInfo.lngLat, | ||||||
|                     } |                     }, | ||||||
|                 }; |                 } | ||||||
|                 features.push(newPoint) |                 features.push(newPoint) | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|  | @ -113,18 +113,20 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|                 const moveDescription = { |                 const moveDescription = { | ||||||
|                     type: "Feature", |                     type: "Feature", | ||||||
|                     properties: { |                     properties: { | ||||||
|                         "move": "yes", |                         move: "yes", | ||||||
|                         "osm-id": reusedPoint.node.properties.id, |                         "osm-id": reusedPoint.node.properties.id, | ||||||
|                         "id": "new-geometry-move-existing" + i, |                         id: "new-geometry-move-existing" + i, | ||||||
|                         "distance": GeoOperations.distanceBetween(coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates) |                         distance: GeoOperations.distanceBetween( | ||||||
|  |                             coordinateInfo.lngLat, | ||||||
|  |                             reusedPoint.node.geometry.coordinates | ||||||
|  |                         ), | ||||||
|                     }, |                     }, | ||||||
|                     geometry: { |                     geometry: { | ||||||
|                         type: "LineString", |                         type: "LineString", | ||||||
|                         coordinates: [reusedPoint.node.geometry.coordinates, coordinateInfo.lngLat] |                         coordinates: [reusedPoint.node.geometry.coordinates, coordinateInfo.lngLat], | ||||||
|                     } |                     }, | ||||||
|                 } |                 } | ||||||
|                 features.push(moveDescription) |                 features.push(moveDescription) | ||||||
| 
 |  | ||||||
|             } else { |             } else { | ||||||
|                 // The geometry is moved, the point is reused
 |                 // The geometry is moved, the point is reused
 | ||||||
|                 geometryMoved = true |                 geometryMoved = true | ||||||
|  | @ -132,22 +134,24 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|                 const reuseDescription = { |                 const reuseDescription = { | ||||||
|                     type: "Feature", |                     type: "Feature", | ||||||
|                     properties: { |                     properties: { | ||||||
|                         "move": "no", |                         move: "no", | ||||||
|                         "osm-id": reusedPoint.node.properties.id, |                         "osm-id": reusedPoint.node.properties.id, | ||||||
|                         "id": "new-geometry-reuse-existing" + i, |                         id: "new-geometry-reuse-existing" + i, | ||||||
|                         "distance": GeoOperations.distanceBetween(coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates) |                         distance: GeoOperations.distanceBetween( | ||||||
|  |                             coordinateInfo.lngLat, | ||||||
|  |                             reusedPoint.node.geometry.coordinates | ||||||
|  |                         ), | ||||||
|                     }, |                     }, | ||||||
|                     geometry: { |                     geometry: { | ||||||
|                         type: "LineString", |                         type: "LineString", | ||||||
|                         coordinates: [coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates] |                         coordinates: [coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates], | ||||||
|                     } |                     }, | ||||||
|                 } |                 } | ||||||
|                 features.push(reuseDescription) |                 features.push(reuseDescription) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (geometryMoved) { |         if (geometryMoved) { | ||||||
| 
 |  | ||||||
|             const coords: [number, number][] = [] |             const coords: [number, number][] = [] | ||||||
|             for (const info of this._coordinateInfo) { |             for (const info of this._coordinateInfo) { | ||||||
|                 if (info.identicalTo !== undefined) { |                 if (info.identicalTo !== undefined) { | ||||||
|  | @ -166,21 +170,19 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|                 } else { |                 } else { | ||||||
|                     coords.push(info.lngLat) |                     coords.push(info.lngLat) | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|             const newGeometry = { |             const newGeometry = { | ||||||
|                 type: "Feature", |                 type: "Feature", | ||||||
|                 properties: { |                 properties: { | ||||||
|                     "resulting-geometry": "yes", |                     "resulting-geometry": "yes", | ||||||
|                     "id": "new-geometry" |                     id: "new-geometry", | ||||||
|                 }, |                 }, | ||||||
|                 geometry: { |                 geometry: { | ||||||
|                     type: "LineString", |                     type: "LineString", | ||||||
|                     coordinates: coords |                     coordinates: coords, | ||||||
|                 } |                 }, | ||||||
|             } |             } | ||||||
|             features.push(newGeometry) |             features.push(newGeometry) | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
|         return StaticFeatureSource.fromGeojson(features) |         return StaticFeatureSource.fromGeojson(features) | ||||||
|     } |     } | ||||||
|  | @ -188,7 +190,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
|         const theme = this._state?.layoutToUse?.id |         const theme = this._state?.layoutToUse?.id | ||||||
|         const allChanges: ChangeDescription[] = [] |         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++) { |         for (let i = 0; i < this._coordinateInfo.length; i++) { | ||||||
|             const info = this._coordinateInfo[i] |             const info = this._coordinateInfo[i] | ||||||
|             const lat = info.lngLat[1] |             const lat = info.lngLat[1] | ||||||
|  | @ -202,17 +204,17 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|                 const newNodeAction = new CreateNewNodeAction([], lat, lon, { |                 const newNodeAction = new CreateNewNodeAction([], lat, lon, { | ||||||
|                     allowReuseOfPreviouslyCreatedPoints: true, |                     allowReuseOfPreviouslyCreatedPoints: true, | ||||||
|                     changeType: null, |                     changeType: null, | ||||||
|                     theme |                     theme, | ||||||
|                 }) |                 }) | ||||||
| 
 | 
 | ||||||
|                 allChanges.push(...(await newNodeAction.CreateChangeDescriptions(changes))) |                 allChanges.push(...(await newNodeAction.CreateChangeDescriptions(changes))) | ||||||
| 
 | 
 | ||||||
|                 nodeIdsToUse.push({ |                 nodeIdsToUse.push({ | ||||||
|                     lat, lon, |                     lat, | ||||||
|                     nodeId: newNodeAction.newElementIdNumber |                     lon, | ||||||
|  |                     nodeId: newNodeAction.newElementIdNumber, | ||||||
|                 }) |                 }) | ||||||
|                 continue |                 continue | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const closestPoint = info.closebyNodes[0] |             const closestPoint = info.closebyNodes[0] | ||||||
|  | @ -222,20 +224,20 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|                     type: "node", |                     type: "node", | ||||||
|                     id, |                     id, | ||||||
|                     changes: { |                     changes: { | ||||||
|                         lat, lon |                         lat, | ||||||
|  |                         lon, | ||||||
|                     }, |                     }, | ||||||
|                     meta: { |                     meta: { | ||||||
|                         theme, |                         theme, | ||||||
|                         changeType: null |                         changeType: null, | ||||||
|                     } |                     }, | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|             nodeIdsToUse.push({lat, lon, nodeId: id}) |             nodeIdsToUse.push({ lat, lon, nodeId: id }) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, { |         const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, { | ||||||
|             theme |             theme, | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         allChanges.push(...(await newWay.CreateChangeDescriptions(changes))) |         allChanges.push(...(await newWay.CreateChangeDescriptions(changes))) | ||||||
|  | @ -248,27 +250,26 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|      * Calculates the main changes. |      * Calculates the main changes. | ||||||
|      */ |      */ | ||||||
|     private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] { |     private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] { | ||||||
| 
 |  | ||||||
|         const bbox = new BBox(coordinates) |         const bbox = new BBox(coordinates) | ||||||
|         const state = this._state |         const state = this._state | ||||||
|         const allNodes = [].concat(...state?.featurePipeline?.GetFeaturesWithin("type_node", bbox.pad(1.2))??[]) |         const allNodes = [].concat( | ||||||
|         const maxDistance = Math.max(...this._config.map(c => c.withinRangeOfM)) |             ...(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
 |         // Init coordianteinfo with undefined but the same length as coordinates
 | ||||||
|         const coordinateInfo: { |         const coordinateInfo: { | ||||||
|             lngLat: [number, number], |             lngLat: [number, number] | ||||||
|             identicalTo?: number, |             identicalTo?: number | ||||||
|             closebyNodes?: { |             closebyNodes?: { | ||||||
|                 d: number, |                 d: number | ||||||
|                 node: any, |                 node: any | ||||||
|                 config: MergePointConfig |                 config: MergePointConfig | ||||||
|             }[] |             }[] | ||||||
|         }[] = coordinates.map(_ => undefined) |         }[] = coordinates.map((_) => undefined) | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|         // First loop: gather all information...
 |         // First loop: gather all information...
 | ||||||
|         for (let i = 0; i < coordinates.length; i++) { |         for (let i = 0; i < coordinates.length; i++) { | ||||||
| 
 |  | ||||||
|             if (coordinateInfo[i] !== undefined) { |             if (coordinateInfo[i] !== undefined) { | ||||||
|                 // Already seen, probably a duplicate coordinate
 |                 // Already seen, probably a duplicate coordinate
 | ||||||
|                 continue |                 continue | ||||||
|  | @ -282,9 +283,9 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|                 if (GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) { |                 if (GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) { | ||||||
|                     coordinateInfo[j] = { |                     coordinateInfo[j] = { | ||||||
|                         lngLat: coor, |                         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
 |             // Lets search applicable points and determine the merge mode
 | ||||||
|             const closebyNodes: { |             const closebyNodes: { | ||||||
|                 d: number, |                 d: number | ||||||
|                 node: any, |                 node: any | ||||||
|                 config: MergePointConfig |                 config: MergePointConfig | ||||||
|             }[] = [] |             }[] = [] | ||||||
|             for (const node of allNodes) { |             for (const node of allNodes) { | ||||||
|  | @ -310,7 +311,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|                     if (!config.ifMatches.matchesProperties(node.properties)) { |                     if (!config.ifMatches.matchesProperties(node.properties)) { | ||||||
|                         continue |                         continue | ||||||
|                     } |                     } | ||||||
|                     closebyNodes.push({node, d, config}) |                     closebyNodes.push({ node, d, config }) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -322,18 +323,15 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|             coordinateInfo[i] = { |             coordinateInfo[i] = { | ||||||
|                 identicalTo: undefined, |                 identicalTo: undefined, | ||||||
|                 lngLat: coor, |                 lngLat: coor, | ||||||
|                 closebyNodes |                 closebyNodes, | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         // Second loop: figure out which point moves where without creating conflicts
 |         // Second loop: figure out which point moves where without creating conflicts
 | ||||||
|         let conflictFree = true; |         let conflictFree = true | ||||||
|         do { |         do { | ||||||
|             conflictFree = true; |             conflictFree = true | ||||||
|             for (let i = 0; i < coordinateInfo.length; i++) { |             for (let i = 0; i < coordinateInfo.length; i++) { | ||||||
| 
 |  | ||||||
|                 const coorInfo = coordinateInfo[i] |                 const coorInfo = coordinateInfo[i] | ||||||
|                 if (coorInfo.identicalTo !== undefined) { |                 if (coorInfo.identicalTo !== undefined) { | ||||||
|                     continue |                     continue | ||||||
|  | @ -366,8 +364,6 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | ||||||
|             } |             } | ||||||
|         } while (!conflictFree) |         } while (!conflictFree) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         return coordinateInfo |         return coordinateInfo | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,61 +1,61 @@ | ||||||
| import {OsmObject} from "../OsmObject"; | import { OsmObject } from "../OsmObject" | ||||||
| import OsmChangeAction from "./OsmChangeAction"; | import OsmChangeAction from "./OsmChangeAction" | ||||||
| import {Changes} from "../Changes"; | import { Changes } from "../Changes" | ||||||
| import {ChangeDescription} from "./ChangeDescription"; | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import ChangeTagAction from "./ChangeTagAction"; | import ChangeTagAction from "./ChangeTagAction" | ||||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | import { TagsFilter } from "../../Tags/TagsFilter" | ||||||
| import {And} from "../../Tags/And"; | import { And } from "../../Tags/And" | ||||||
| import {Tag} from "../../Tags/Tag"; | import { Tag } from "../../Tags/Tag" | ||||||
| 
 | 
 | ||||||
| export default class DeleteAction extends OsmChangeAction { | export default class DeleteAction extends OsmChangeAction { | ||||||
| 
 |     private readonly _softDeletionTags: TagsFilter | ||||||
|     private readonly _softDeletionTags: TagsFilter; |  | ||||||
|     private readonly meta: { |     private readonly meta: { | ||||||
|         theme: string, |         theme: string | ||||||
|         specialMotivation: string, |         specialMotivation: string | ||||||
|         changeType: "deletion" |         changeType: "deletion" | ||||||
|     }; |     } | ||||||
|     private readonly _id: string; |     private readonly _id: string | ||||||
|     private _hardDelete: boolean; |     private _hardDelete: boolean | ||||||
| 
 | 
 | ||||||
| 
 |     constructor( | ||||||
|     constructor(id: string, |         id: string, | ||||||
|                 softDeletionTags: TagsFilter, |         softDeletionTags: TagsFilter, | ||||||
|                 meta: { |         meta: { | ||||||
|                     theme: string, |             theme: string | ||||||
|                     specialMotivation: string |             specialMotivation: string | ||||||
|                 }, |         }, | ||||||
|                 hardDelete: boolean) { |         hardDelete: boolean | ||||||
|  |     ) { | ||||||
|         super(id, true) |         super(id, true) | ||||||
|         this._id = id; |         this._id = id | ||||||
|         this._hardDelete = hardDelete; |         this._hardDelete = hardDelete | ||||||
|         this.meta = {...meta, changeType: "deletion"}; |         this.meta = { ...meta, changeType: "deletion" } | ||||||
|         this._softDeletionTags = new And([softDeletionTags, |         this._softDeletionTags = new And([ | ||||||
|             new Tag("fixme", `A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})`) |             softDeletionTags, | ||||||
|         ]); |             new Tag( | ||||||
| 
 |                 "fixme", | ||||||
|  |                 `A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})` | ||||||
|  |             ), | ||||||
|  |         ]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
| 
 |  | ||||||
|         const osmObject = await OsmObject.DownloadObjectAsync(this._id) |         const osmObject = await OsmObject.DownloadObjectAsync(this._id) | ||||||
| 
 | 
 | ||||||
|         if (this._hardDelete) { |         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, |  | ||||||
|                 { |                 { | ||||||
|                     ...this.meta, |                     meta: this.meta, | ||||||
|                     changeType: "soft-delete" |                     doDelete: true, | ||||||
|                 } |                     type: osmObject.type, | ||||||
|             ).CreateChangeDescriptions(changes) |                     id: osmObject.id, | ||||||
|  |                 }, | ||||||
|  |             ] | ||||||
|  |         } else { | ||||||
|  |             return await new ChangeTagAction(this._id, this._softDeletionTags, osmObject.tags, { | ||||||
|  |                 ...this.meta, | ||||||
|  |                 changeType: "soft-delete", | ||||||
|  |             }).CreateChangeDescriptions(changes) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -2,22 +2,21 @@ | ||||||
|  * An action is a change to the OSM-database |  * 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 |  * It will generate some new/modified/deleted objects, which are all bundled by the 'changes'-object | ||||||
|  */ |  */ | ||||||
| import {Changes} from "../Changes"; | import { Changes } from "../Changes" | ||||||
| import {ChangeDescription} from "./ChangeDescription"; | import { ChangeDescription } from "./ChangeDescription" | ||||||
| 
 | 
 | ||||||
| export default abstract class OsmChangeAction { | 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. |      * The ID of the object that is the center of this change. | ||||||
|      * Null if the action creates a new object (at initialization) |      * Null if the action creates a new object (at initialization) | ||||||
|      * Undefined if such an id does not make sense |      * Undefined if such an id does not make sense | ||||||
|      */ |      */ | ||||||
|     public readonly mainObjectId: string; |     public readonly mainObjectId: string | ||||||
|     private isUsed = false |     private isUsed = false | ||||||
| 
 | 
 | ||||||
|     constructor(mainObjectId: string, trackStatistics: boolean = true) { |     constructor(mainObjectId: string, trackStatistics: boolean = true) { | ||||||
|         this.trackStatistics = trackStatistics; |         this.trackStatistics = trackStatistics | ||||||
|         this.mainObjectId = mainObjectId |         this.mainObjectId = mainObjectId | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -25,7 +24,7 @@ export default abstract class OsmChangeAction { | ||||||
|         if (this.isUsed) { |         if (this.isUsed) { | ||||||
|             throw "This ChangeAction is already used" |             throw "This ChangeAction is already used" | ||||||
|         } |         } | ||||||
|         this.isUsed = true; |         this.isUsed = true | ||||||
|         return this.CreateChangeDescriptions(changes) |         return this.CreateChangeDescriptions(changes) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -33,8 +32,6 @@ export default abstract class OsmChangeAction { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export abstract class OsmCreateAction extends OsmChangeAction { | export abstract class OsmCreateAction extends OsmChangeAction { | ||||||
| 
 |  | ||||||
|     public newElementId: string |     public newElementId: string | ||||||
|     public newElementIdNumber: number |     public newElementIdNumber: number | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,24 +1,24 @@ | ||||||
| import OsmChangeAction from "./OsmChangeAction"; | import OsmChangeAction from "./OsmChangeAction" | ||||||
| import {Changes} from "../Changes"; | import { Changes } from "../Changes" | ||||||
| import {ChangeDescription} from "./ChangeDescription"; | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import {OsmObject, OsmRelation, OsmWay} from "../OsmObject"; | import { OsmObject, OsmRelation, OsmWay } from "../OsmObject" | ||||||
| 
 | 
 | ||||||
| export interface RelationSplitInput { | export interface RelationSplitInput { | ||||||
|     relation: OsmRelation, |     relation: OsmRelation | ||||||
|     originalWayId: number, |     originalWayId: number | ||||||
|     allWayIdsInOrder: number[], |     allWayIdsInOrder: number[] | ||||||
|     originalNodes: number[], |     originalNodes: number[] | ||||||
|     allWaysNodesInOrder: number[][] |     allWaysNodesInOrder: number[][] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| abstract class AbstractRelationSplitHandler extends OsmChangeAction { | abstract class AbstractRelationSplitHandler extends OsmChangeAction { | ||||||
|     protected readonly _input: RelationSplitInput; |     protected readonly _input: RelationSplitInput | ||||||
|     protected readonly _theme: string; |     protected readonly _theme: string | ||||||
| 
 | 
 | ||||||
|     constructor(input: RelationSplitInput, theme: string) { |     constructor(input: RelationSplitInput, theme: string) { | ||||||
|         super("relation/" + input.relation.id, false) |         super("relation/" + input.relation.id, false) | ||||||
|         this._input = input; |         this._input = input | ||||||
|         this._theme = theme; |         this._theme = theme | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -44,7 +44,7 @@ abstract class AbstractRelationSplitHandler extends OsmChangeAction { | ||||||
|         if (member.type === "relation") { |         if (member.type === "relation") { | ||||||
|             return undefined |             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. |  * 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 { | export default class RelationSplitHandler extends AbstractRelationSplitHandler { | ||||||
| 
 |  | ||||||
|     constructor(input: RelationSplitInput, theme: string) { |     constructor(input: RelationSplitInput, theme: string) { | ||||||
|         super(input, theme) |         super(input, theme) | ||||||
|     } |     } | ||||||
|  | @ -60,38 +59,43 @@ export default class RelationSplitHandler extends AbstractRelationSplitHandler { | ||||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
|         if (this._input.relation.tags["type"] === "restriction") { |         if (this._input.relation.tags["type"] === "restriction") { | ||||||
|             // This is a turn 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 { | export class TurnRestrictionRSH extends AbstractRelationSplitHandler { | ||||||
| 
 |  | ||||||
|     constructor(input: RelationSplitInput, theme: string) { |     constructor(input: RelationSplitInput, theme: string) { | ||||||
|         super(input, theme); |         super(input, theme) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
| 
 |  | ||||||
|         const relation = this._input.relation |         const relation = this._input.relation | ||||||
|         const members = relation.members |         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) { |         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] |         const selfMember = selfMembers[0] | ||||||
| 
 | 
 | ||||||
|         if (selfMember.role === "via") { |         if (selfMember.role === "via") { | ||||||
|             // A via way can be replaced in place
 |             // 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
 |         // 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
 |         // Let's figure out which member is neighbouring our way
 | ||||||
| 
 | 
 | ||||||
|  | @ -102,11 +106,12 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler { | ||||||
|         let commonPoint = commonStartPoint ?? commonEndPoint |         let commonPoint = commonStartPoint ?? commonEndPoint | ||||||
| 
 | 
 | ||||||
|         // Let's select the way to keep
 |         // Let's select the way to keep
 | ||||||
|         const idToKeep: { id: number } = this._input.allWaysNodesInOrder.map((nodes, i) => ({ |         const idToKeep: { id: number } = this._input.allWaysNodesInOrder | ||||||
|             nodes: nodes, |             .map((nodes, i) => ({ | ||||||
|             id: this._input.allWayIdsInOrder[i] |                 nodes: nodes, | ||||||
|         })) |                 id: this._input.allWayIdsInOrder[i], | ||||||
|             .filter(nodesId => { |             })) | ||||||
|  |             .filter((nodesId) => { | ||||||
|                 const nds = nodesId.nodes |                 const nds = nodesId.nodes | ||||||
|                 return nds[0] == commonPoint || nds[nds.length - 1] == commonPoint |                 return nds[0] == commonPoint || nds[nds.length - 1] == commonPoint | ||||||
|             })[0] |             })[0] | ||||||
|  | @ -123,36 +128,34 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const newMembers: { |         const newMembers: { | ||||||
|             ref: number, |             ref: number | ||||||
|             type: "way" | "node" | "relation", |             type: "way" | "node" | "relation" | ||||||
|             role: string |             role: string | ||||||
|         } [] = relation.members.map(m => { |         }[] = relation.members.map((m) => { | ||||||
|             if (m.type === "way" && m.ref === originalWayId) { |             if (m.type === "way" && m.ref === originalWayId) { | ||||||
|                 return { |                 return { | ||||||
|                     ref: idToKeep.id, |                     ref: idToKeep.id, | ||||||
|                     type: "way", |                     type: "way", | ||||||
|                     role: m.role |                     role: m.role, | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             return m |             return m | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         return [ |         return [ | ||||||
|             { |             { | ||||||
|                 type: "relation", |                 type: "relation", | ||||||
|                 id: relation.id, |                 id: relation.id, | ||||||
|                 changes: { |                 changes: { | ||||||
|                     members: newMembers |                     members: newMembers, | ||||||
|                 }, |                 }, | ||||||
|                 meta: { |                 meta: { | ||||||
|                     theme: this._theme, |                     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. |  * Note that the feature might appear multiple times. | ||||||
|  */ |  */ | ||||||
| export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { | export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { | ||||||
| 
 |  | ||||||
|     constructor(input: RelationSplitInput, theme: string) { |     constructor(input: RelationSplitInput, theme: string) { | ||||||
|         super(input, theme); |         super(input, theme) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
| 
 |  | ||||||
|         const wayId = this._input.originalWayId |         const wayId = this._input.originalWayId | ||||||
|         const relation = this._input.relation |         const relation = this._input.relation | ||||||
|         const members = relation.members |         const members = relation.members | ||||||
|         const originalNodes = this._input.originalNodes; |         const originalNodes = this._input.originalNodes | ||||||
|         const firstNode = originalNodes[0] |         const firstNode = originalNodes[0] | ||||||
|         const lastNode = originalNodes[originalNodes.length - 1] |         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++) { |         for (let i = 0; i < members.length; i++) { | ||||||
|             const member = members[i]; |             const member = members[i] | ||||||
|             if (member.type !== "way" || member.ref !== wayId) { |             if (member.type !== "way" || member.ref !== wayId) { | ||||||
|                 newMembers.push(member) |                 newMembers.push(member) | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const nodeIdBefore = await this.targetNodeAt(i - 1, false) |             const nodeIdBefore = await this.targetNodeAt(i - 1, false) | ||||||
|  | @ -197,10 +198,10 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { | ||||||
|                     newMembers.push({ |                     newMembers.push({ | ||||||
|                         ref: wId, |                         ref: wId, | ||||||
|                         type: "way", |                         type: "way", | ||||||
|                         role: member.role |                         role: member.role, | ||||||
|                     }) |                     }) | ||||||
|                 } |                 } | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const firstNodeMatchesRev = nodeIdBefore === undefined || nodeIdBefore === lastNode |             const firstNodeMatchesRev = nodeIdBefore === undefined || nodeIdBefore === lastNode | ||||||
|  | @ -209,14 +210,14 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { | ||||||
|                 // We (probably) have a reversed situation, backward situation
 |                 // We (probably) have a reversed situation, backward situation
 | ||||||
|                 for (let i1 = this._input.allWayIdsInOrder.length - 1; i1 >= 0; i1--) { |                 for (let i1 = this._input.allWayIdsInOrder.length - 1; i1 >= 0; i1--) { | ||||||
|                     // Iterate BACKWARDS
 |                     // Iterate BACKWARDS
 | ||||||
|                     const wId = this._input.allWayIdsInOrder[i1]; |                     const wId = this._input.allWayIdsInOrder[i1] | ||||||
|                     newMembers.push({ |                     newMembers.push({ | ||||||
|                         ref: wId, |                         ref: wId, | ||||||
|                         type: "way", |                         type: "way", | ||||||
|                         role: member.role |                         role: member.role, | ||||||
|                     }) |                     }) | ||||||
|                 } |                 } | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Euhm, allright... Something weird is going on, but let's not care too much
 |             // 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({ |                 newMembers.push({ | ||||||
|                     ref: wId, |                     ref: wId, | ||||||
|                     type: "way", |                     type: "way", | ||||||
|                     role: member.role |                     role: member.role, | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return [{ |         return [ | ||||||
|             id: relation.id, |             { | ||||||
|             type: "relation", |                 id: relation.id, | ||||||
|             changes: {members: newMembers}, |                 type: "relation", | ||||||
|             meta: { |                 changes: { members: newMembers }, | ||||||
|                 changeType: "relation-fix", |                 meta: { | ||||||
|                 theme: this._theme |                     changeType: "relation-fix", | ||||||
|             } |                     theme: this._theme, | ||||||
|         }]; |                 }, | ||||||
|  |             }, | ||||||
|  |         ] | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,59 +1,59 @@ | ||||||
| import OsmChangeAction from "./OsmChangeAction"; | import OsmChangeAction from "./OsmChangeAction" | ||||||
| import {Changes} from "../Changes"; | import { Changes } from "../Changes" | ||||||
| import {ChangeDescription} from "./ChangeDescription"; | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import {Tag} from "../../Tags/Tag"; | import { Tag } from "../../Tags/Tag" | ||||||
| import FeatureSource from "../../FeatureSource/FeatureSource"; | import FeatureSource from "../../FeatureSource/FeatureSource" | ||||||
| import {OsmNode, OsmObject, OsmWay} from "../OsmObject"; | import { OsmNode, OsmObject, OsmWay } from "../OsmObject" | ||||||
| import {GeoOperations} from "../../GeoOperations"; | import { GeoOperations } from "../../GeoOperations" | ||||||
| import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"; | import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" | ||||||
| import CreateNewNodeAction from "./CreateNewNodeAction"; | import CreateNewNodeAction from "./CreateNewNodeAction" | ||||||
| import ChangeTagAction from "./ChangeTagAction"; | import ChangeTagAction from "./ChangeTagAction" | ||||||
| import {And} from "../../Tags/And"; | import { And } from "../../Tags/And" | ||||||
| import {Utils} from "../../../Utils"; | import { Utils } from "../../../Utils" | ||||||
| import {OsmConnection} from "../OsmConnection"; | import { OsmConnection } from "../OsmConnection" | ||||||
| import {Feature} from "@turf/turf"; | import { Feature } from "@turf/turf" | ||||||
| import FeaturePipeline from "../../FeatureSource/FeaturePipeline"; | import FeaturePipeline from "../../FeatureSource/FeaturePipeline" | ||||||
| 
 | 
 | ||||||
| export default class ReplaceGeometryAction extends OsmChangeAction { | export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|     /** |     /** | ||||||
|      * The target feature - mostly used for the metadata |      * The target feature - mostly used for the metadata | ||||||
|      */ |      */ | ||||||
|     private readonly feature: any; |     private readonly feature: any | ||||||
|     private readonly state: { |     private readonly state: { | ||||||
|         osmConnection: OsmConnection, |         osmConnection: OsmConnection | ||||||
|         featurePipeline: FeaturePipeline |         featurePipeline: FeaturePipeline | ||||||
|     }; |     } | ||||||
|     private readonly wayToReplaceId: string; |     private readonly wayToReplaceId: string | ||||||
|     private readonly theme: string; |     private readonly theme: string | ||||||
|     /** |     /** | ||||||
|      * The target coordinates that should end up in OpenStreetMap. |      * 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] |      * This is identical to either this.feature.geometry.coordinates or -in case of a polygon- feature.geometry.coordinates[0] | ||||||
|      * Format: [lon, lat] |      * 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. |      * If a target coordinate is close to another target coordinate, 'identicalTo' will point to the first index. | ||||||
|      */ |      */ | ||||||
|     private readonly identicalTo: number[] |     private readonly identicalTo: number[] | ||||||
|     private readonly newTags: Tag[] | undefined; |     private readonly newTags: Tag[] | undefined | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         state: { |         state: { | ||||||
|             osmConnection: OsmConnection, |             osmConnection: OsmConnection | ||||||
|             featurePipeline: FeaturePipeline |             featurePipeline: FeaturePipeline | ||||||
|         }, |         }, | ||||||
|         feature: any, |         feature: any, | ||||||
|         wayToReplaceId: string, |         wayToReplaceId: string, | ||||||
|         options: { |         options: { | ||||||
|             theme: string, |             theme: string | ||||||
|             newTags?: Tag[] |             newTags?: Tag[] | ||||||
|         } |         } | ||||||
|     ) { |     ) { | ||||||
|         super(wayToReplaceId, false); |         super(wayToReplaceId, false) | ||||||
|         this.state = state; |         this.state = state | ||||||
|         this.feature = feature; |         this.feature = feature | ||||||
|         this.wayToReplaceId = wayToReplaceId; |         this.wayToReplaceId = wayToReplaceId | ||||||
|         this.theme = options.theme; |         this.theme = options.theme | ||||||
| 
 | 
 | ||||||
|         const geom = this.feature.geometry |         const geom = this.feature.geometry | ||||||
|         let coordinates: [number, number][] |         let coordinates: [number, number][] | ||||||
|  | @ -64,7 +64,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|         } |         } | ||||||
|         this.targetCoordinates = coordinates |         this.targetCoordinates = coordinates | ||||||
| 
 | 
 | ||||||
|         this.identicalTo = coordinates.map(_ => undefined) |         this.identicalTo = coordinates.map((_) => undefined) | ||||||
| 
 | 
 | ||||||
|         for (let i = 0; i < coordinates.length; i++) { |         for (let i = 0; i < coordinates.length; i++) { | ||||||
|             if (this.identicalTo[i] !== undefined) { |             if (this.identicalTo[i] !== undefined) { | ||||||
|  | @ -82,7 +82,8 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
| 
 | 
 | ||||||
|     // noinspection JSUnusedGlobalSymbols
 |     // noinspection JSUnusedGlobalSymbols
 | ||||||
|     public async getPreview(): Promise<FeatureSource> { |     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) => { |         const preview: Feature[] = closestIds.map((newId, i) => { | ||||||
|             if (this.identicalTo[i] !== undefined) { |             if (this.identicalTo[i] !== undefined) { | ||||||
|                 return undefined |                 return undefined | ||||||
|  | @ -92,75 +93,73 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|                 return { |                 return { | ||||||
|                     type: "Feature", |                     type: "Feature", | ||||||
|                     properties: { |                     properties: { | ||||||
|                         "newpoint": "yes", |                         newpoint: "yes", | ||||||
|                         "id": "replace-geometry-move-" + i, |                         id: "replace-geometry-move-" + i, | ||||||
|                     }, |                     }, | ||||||
|                     geometry: { |                     geometry: { | ||||||
|                         type: "Point", |                         type: "Point", | ||||||
|                         coordinates: this.targetCoordinates[i] |                         coordinates: this.targetCoordinates[i], | ||||||
|                     } |                     }, | ||||||
|                 }; |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const origNode = allNodesById.get(newId); |             const origNode = allNodesById.get(newId) | ||||||
|             return { |             return { | ||||||
|                 type: "Feature", |                 type: "Feature", | ||||||
|                 properties: { |                 properties: { | ||||||
|                     "move": "yes", |                     move: "yes", | ||||||
|                     "osm-id": newId, |                     "osm-id": newId, | ||||||
|                     "id": "replace-geometry-move-" + i, |                     id: "replace-geometry-move-" + i, | ||||||
|                     "original-node-tags": JSON.stringify(origNode.tags) |                     "original-node-tags": JSON.stringify(origNode.tags), | ||||||
|                 }, |                 }, | ||||||
|                 geometry: { |                 geometry: { | ||||||
|                     type: "LineString", |                     type: "LineString", | ||||||
|                     coordinates: [[origNode.lon, origNode.lat], this.targetCoordinates[i]] |                     coordinates: [[origNode.lon, origNode.lat], this.targetCoordinates[i]], | ||||||
|                 } |                 }, | ||||||
|             }; |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 |         reprojectedNodes.forEach(({ newLat, newLon, nodeId }) => { | ||||||
|         reprojectedNodes.forEach(({newLat, newLon, nodeId}) => { |             const origNode = allNodesById.get(nodeId) | ||||||
| 
 |             const feature: Feature = { | ||||||
|             const origNode = allNodesById.get(nodeId); |  | ||||||
|             const feature : Feature =  { |  | ||||||
|                 type: "Feature", |                 type: "Feature", | ||||||
|                 properties: { |                 properties: { | ||||||
|                     "move": "yes", |                     move: "yes", | ||||||
|                     "reprojection": "yes", |                     reprojection: "yes", | ||||||
|                     "osm-id": nodeId, |                     "osm-id": nodeId, | ||||||
|                     "id": "replace-geometry-reproject-" + nodeId, |                     id: "replace-geometry-reproject-" + nodeId, | ||||||
|                     "original-node-tags": JSON.stringify(origNode.tags) |                     "original-node-tags": JSON.stringify(origNode.tags), | ||||||
|                 }, |                 }, | ||||||
|                 geometry: { |                 geometry: { | ||||||
|                     type: "LineString", |                     type: "LineString", | ||||||
|                     coordinates: [[origNode.lon, origNode.lat], [newLon, newLat]] |                     coordinates: [ | ||||||
|                 } |                         [origNode.lon, origNode.lat], | ||||||
|             }; |                         [newLon, newLat], | ||||||
|  |                     ], | ||||||
|  |                 }, | ||||||
|  |             } | ||||||
|             preview.push(feature) |             preview.push(feature) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 |         detachedNodes.forEach(({ reason }, id) => { | ||||||
|         detachedNodes.forEach(({reason}, id) => { |             const origNode = allNodesById.get(id) | ||||||
|             const origNode = allNodesById.get(id); |             const feature: Feature = { | ||||||
|             const feature : Feature = { |  | ||||||
|                 type: "Feature", |                 type: "Feature", | ||||||
|                 properties: { |                 properties: { | ||||||
|                     "detach": "yes", |                     detach: "yes", | ||||||
|                     "id": "replace-geometry-detach-" + id, |                     id: "replace-geometry-detach-" + id, | ||||||
|                     "detach-reason": reason, |                     "detach-reason": reason, | ||||||
|                     "original-node-tags": JSON.stringify(origNode.tags) |                     "original-node-tags": JSON.stringify(origNode.tags), | ||||||
|                 }, |                 }, | ||||||
|                 geometry: { |                 geometry: { | ||||||
|                     type: "Point", |                     type: "Point", | ||||||
|                     coordinates: [origNode.lon, origNode.lat] |                     coordinates: [origNode.lon, origNode.lat], | ||||||
|                 } |                 }, | ||||||
|             }; |             } | ||||||
|             preview.push(feature) |             preview.push(feature) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         return StaticFeatureSource.fromGeojson(Utils.NoNull(preview)) |         return StaticFeatureSource.fromGeojson(Utils.NoNull(preview)) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -170,45 +169,52 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|      * |      * | ||||||
|      */ |      */ | ||||||
|     public async GetClosestIds(): Promise<{ |     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
 |         // A list of the same length as targetCoordinates, containing which OSM-point to move. If undefined, a new point will be created
 | ||||||
|         closestIds: number[], |         closestIds: number[] | ||||||
|         allNodesById: Map<number, OsmNode>, |         allNodesById: Map<number, OsmNode> | ||||||
|         osmWay: OsmWay, |         osmWay: OsmWay | ||||||
|         detachedNodes: Map<number, { |         detachedNodes: Map< | ||||||
|             reason: string, |             number, | ||||||
|             hasTags: boolean |             { | ||||||
|         }>, |                 reason: string | ||||||
|         reprojectedNodes: Map<number, { |                 hasTags: boolean | ||||||
|             /*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, |         reprojectedNodes: Map< | ||||||
|             newLat: number, |             number, | ||||||
|             newLon: number, |             { | ||||||
|             nodeId: 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 | ||||||
|  |                 nodeId: number | ||||||
|  |             } | ||||||
|  |         > | ||||||
|     }> { |     }> { | ||||||
|         // TODO FIXME: if a new point has to be created, snap to already existing ways
 |         // 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) { |         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)" |             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; |         const self = this | ||||||
|         let parsed: OsmObject[]; |         let parsed: OsmObject[] | ||||||
|         { |         { | ||||||
|             // Gather the needed OsmObjects
 |             // Gather the needed OsmObjects
 | ||||||
|             const splitted = this.wayToReplaceId.split("/"); |             const splitted = this.wayToReplaceId.split("/") | ||||||
|             const type = splitted[0]; |             const type = splitted[0] | ||||||
|             const idN = Number(splitted[1]); |             const idN = Number(splitted[1]) | ||||||
|             if (idN < 0 || type !== "way") { |             if (idN < 0 || type !== "way") { | ||||||
|                 throw "Invalid ID to conflate: " + this.wayToReplaceId |                 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) |             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] |         const osmWay = <OsmWay>parsed[parsed.length - 1] | ||||||
|         if (osmWay.type !== "way") { |         if (osmWay.type !== "way") { | ||||||
|             throw "WEIRD: expected an OSM-way as last element here!" |             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 |          * 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)*/ |             /** target coordinate index --> distance (or undefined if a duplicate)*/ | ||||||
|             number[]>(); |             number[] | ||||||
|  |         >() | ||||||
| 
 | 
 | ||||||
|         const nodeInfo = new Map<number /* osmId*/, { |         const nodeInfo = new Map< | ||||||
|             distances: number[], |             number /* osmId*/, | ||||||
|             // Part of some other way then the one that should be replaced
 |             { | ||||||
|             partOfWay: boolean, |                 distances: number[] | ||||||
|             hasTags: boolean |                 // Part of some other way then the one that should be replaced
 | ||||||
|         }>() |                 partOfWay: boolean | ||||||
|  |                 hasTags: boolean | ||||||
|  |             } | ||||||
|  |         >() | ||||||
| 
 | 
 | ||||||
|         for (const node of allNodes) { |         for (const node of allNodes) { | ||||||
| 
 |  | ||||||
|             const parentWays = nodeDb.GetParentWays(node.id) |             const parentWays = nodeDb.GetParentWays(node.id) | ||||||
|             if (parentWays === undefined) { |             if (parentWays === undefined) { | ||||||
|                 throw "PANIC: the way to replace has a node which has no parents at all. Is it deleted in the meantime?" |                 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) |             const idIndex = parentWayIds.indexOf(this.wayToReplaceId) | ||||||
|             if (idIndex < 0) { |             if (idIndex < 0) { | ||||||
|                 throw "PANIC: the way to replace has a node, which is _not_ part of this was according to the node..." |                 throw "PANIC: the way to replace has a node, which is _not_ part of this was according to the node..." | ||||||
|             } |             } | ||||||
|             parentWayIds.splice(idIndex, 1) |             parentWayIds.splice(idIndex, 1) | ||||||
|             const partOfSomeWay = parentWayIds.length > 0 |             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++) { |             for (let i = 0; i < this.targetCoordinates.length; i++) { | ||||||
|                 if (this.identicalTo[i] !== undefined) { |                 if (this.identicalTo[i] !== undefined) { | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
|                 const targetCoordinate = this.targetCoordinates[i]; |                 const targetCoordinate = this.targetCoordinates[i] | ||||||
|                 const cp = node.centerpoint() |                 const cp = node.centerpoint() | ||||||
|                 const d = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]]) |                 const d = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]]) | ||||||
|                 if (d > 25) { |                 if (d > 25) { | ||||||
|  | @ -268,37 +278,39 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|                 } |                 } | ||||||
|                 if (d < 3 || !(hasTags || partOfSomeWay)) { |                 if (d < 3 || !(hasTags || partOfSomeWay)) { | ||||||
|                     // If there is some relation: cap the move distance to 3m
 |                     // If there is some relation: cap the move distance to 3m
 | ||||||
|                     nodeDistances[i] = d; |                     nodeDistances[i] = d | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|             distances.set(node.id, nodeDistances) |             distances.set(node.id, nodeDistances) | ||||||
|             nodeInfo.set(node.id, { |             nodeInfo.set(node.id, { | ||||||
|                 distances: nodeDistances, |                 distances: nodeDistances, | ||||||
|                 partOfWay: partOfSomeWay, |                 partOfWay: partOfSomeWay, | ||||||
|                 hasTags |                 hasTags, | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const closestIds = this.targetCoordinates.map(_ => undefined) |         const closestIds = this.targetCoordinates.map((_) => undefined) | ||||||
|         const unusedIds = new Map<number, { |         const unusedIds = new Map< | ||||||
|             reason: string, |             number, | ||||||
|             hasTags: boolean |             { | ||||||
|         }>(); |                 reason: string | ||||||
|  |                 hasTags: boolean | ||||||
|  |             } | ||||||
|  |         >() | ||||||
|         { |         { | ||||||
|             // Search best merge candidate
 |             // Search best merge candidate
 | ||||||
|             /** |             /** | ||||||
|              * Then, we search the node that has to move the least distance and add this as mapping. |              * 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 |              * We do this until no points are left | ||||||
|              */ |              */ | ||||||
|             let candidate: number; |             let candidate: number | ||||||
|             let moveDistance: number; |             let moveDistance: number | ||||||
|             /** |             /** | ||||||
|              * The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates |              * The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates | ||||||
|              */ |              */ | ||||||
|             do { |             do { | ||||||
|                 candidate = undefined; |                 candidate = undefined | ||||||
|                 moveDistance = Infinity; |                 moveDistance = Infinity | ||||||
|                 distances.forEach((distances, nodeId) => { |                 distances.forEach((distances, nodeId) => { | ||||||
|                     const minDist = Math.min(...Utils.NoNull(distances)) |                     const minDist = Math.min(...Utils.NoNull(distances)) | ||||||
|                     if (moveDistance > minDist) { |                     if (moveDistance > minDist) { | ||||||
|  | @ -310,14 +322,14 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
| 
 | 
 | ||||||
|                 if (candidate !== undefined) { |                 if (candidate !== undefined) { | ||||||
|                     // We found a candidate... Search the corresponding target id:
 |                     // We found a candidate... Search the corresponding target id:
 | ||||||
|                     let targetId: number = undefined; |                     let targetId: number = undefined | ||||||
|                     let lowestDistance = Number.MAX_VALUE |                     let lowestDistance = Number.MAX_VALUE | ||||||
|                     let nodeDistances = distances.get(candidate) |                     let nodeDistances = distances.get(candidate) | ||||||
|                     for (let i = 0; i < nodeDistances.length; i++) { |                     for (let i = 0; i < nodeDistances.length; i++) { | ||||||
|                         const d = nodeDistances[i] |                         const d = nodeDistances[i] | ||||||
|                         if (d !== undefined && d < lowestDistance) { |                         if (d !== undefined && d < lowestDistance) { | ||||||
|                             lowestDistance = d; |                             lowestDistance = d | ||||||
|                             targetId = i; |                             targetId = i | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|  | @ -330,14 +342,14 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|                         closestIds[targetId] = candidate |                         closestIds[targetId] = candidate | ||||||
| 
 | 
 | ||||||
|                         // To indicate that this targetCoordinate is taken, we remove them from the distances matrix
 |                         // To indicate that this targetCoordinate is taken, we remove them from the distances matrix
 | ||||||
|                         distances.forEach(dists => { |                         distances.forEach((dists) => { | ||||||
|                             dists[targetId] = undefined |                             dists[targetId] = undefined | ||||||
|                         }) |                         }) | ||||||
|                     } else { |                     } else { | ||||||
|                         // Seems like all the targetCoordinates have found a source point
 |                         // Seems like all the targetCoordinates have found a source point
 | ||||||
|                         unusedIds.set(candidate, { |                         unusedIds.set(candidate, { | ||||||
|                             reason: "Unused by new way", |                             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) => { |         distances.forEach((_, nodeId) => { | ||||||
|             unusedIds.set(nodeId, { |             unusedIds.set(nodeId, { | ||||||
|                 reason: "Unused by new way", |                 reason: "Unused by new way", | ||||||
|                 hasTags: nodeInfo.get(nodeId).hasTags |                 hasTags: nodeInfo.get(nodeId).hasTags, | ||||||
|             }) |             }) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const reprojectedNodes = new Map<number, { |         const reprojectedNodes = new Map< | ||||||
|             /*Move the node with this ID into the way as extra node, as it has some relation with the original object*/ |             number, | ||||||
|             projectAfterIndex: number, |             { | ||||||
|             distance: number, |                 /*Move the node with this ID into the way as extra node, as it has some relation with the original object*/ | ||||||
|             newLat: number, |                 projectAfterIndex: number | ||||||
|             newLon: number, |                 distance: number | ||||||
|             nodeId: 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?
 |             // Lets check the unused ids: can they be detached or do they signify some relation with the object?
 | ||||||
|             unusedIds.forEach(({}, id) => { |             unusedIds.forEach(({}, id) => { | ||||||
|  | @ -379,36 +394,32 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|                     properties: {}, |                     properties: {}, | ||||||
|                     geometry: { |                     geometry: { | ||||||
|                         type: "LineString", |                         type: "LineString", | ||||||
|                         coordinates: self.targetCoordinates |                         coordinates: self.targetCoordinates, | ||||||
|                     } |                     }, | ||||||
|                 }; |                 } | ||||||
|                 const projected = GeoOperations.nearestPoint( |                 const projected = GeoOperations.nearestPoint(way, [node.lon, node.lat]) | ||||||
|                     way, [node.lon, node.lat] |  | ||||||
|                 ) |  | ||||||
|                 reprojectedNodes.set(id, { |                 reprojectedNodes.set(id, { | ||||||
|                     newLon: projected.geometry.coordinates[0], |                     newLon: projected.geometry.coordinates[0], | ||||||
|                     newLat: projected.geometry.coordinates[1], |                     newLat: projected.geometry.coordinates[1], | ||||||
|                     projectAfterIndex: projected.properties.index, |                     projectAfterIndex: projected.properties.index, | ||||||
|                     distance: projected.properties.dist, |                     distance: projected.properties.dist, | ||||||
|                     nodeId: id |                     nodeId: id, | ||||||
|                 }) |                 }) | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
|             reprojectedNodes.forEach((_, nodeId) => unusedIds.delete(nodeId)) |             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[]> { |     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
|         const nodeDb = this.state.featurePipeline.fullNodeDatabase; |         const nodeDb = this.state.featurePipeline.fullNodeDatabase | ||||||
|         if (nodeDb === undefined) { |         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)" |             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 allChanges: ChangeDescription[] = [] | ||||||
|         const actualIdsToUse: number[] = [] |         const actualIdsToUse: number[] = [] | ||||||
|         for (let i = 0; i < closestIds.length; i++) { |         for (let i = 0; i < closestIds.length; i++) { | ||||||
|  | @ -417,47 +428,43 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|                 actualIdsToUse.push(actualIdsToUse[j]) |                 actualIdsToUse.push(actualIdsToUse[j]) | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             const closestId = closestIds[i]; |             const closestId = closestIds[i] | ||||||
|             const [lon, lat] = this.targetCoordinates[i] |             const [lon, lat] = this.targetCoordinates[i] | ||||||
|             if (closestId === undefined) { |             if (closestId === undefined) { | ||||||
| 
 |                 const newNodeAction = new CreateNewNodeAction([], lat, lon, { | ||||||
|                 const newNodeAction = new CreateNewNodeAction( |                     allowReuseOfPreviouslyCreatedPoints: true, | ||||||
|                     [], |                     theme: this.theme, | ||||||
|                     lat, lon, |                     changeType: null, | ||||||
|                     { |                 }) | ||||||
|                         allowReuseOfPreviouslyCreatedPoints: true, |  | ||||||
|                         theme: this.theme, changeType: null |  | ||||||
|                     }) |  | ||||||
|                 const changeDescr = await newNodeAction.CreateChangeDescriptions(changes) |                 const changeDescr = await newNodeAction.CreateChangeDescriptions(changes) | ||||||
|                 allChanges.push(...changeDescr) |                 allChanges.push(...changeDescr) | ||||||
|                 actualIdsToUse.push(newNodeAction.newElementIdNumber) |                 actualIdsToUse.push(newNodeAction.newElementIdNumber) | ||||||
| 
 |  | ||||||
|             } else { |             } else { | ||||||
|                 const change = <ChangeDescription>{ |                 const change = <ChangeDescription>{ | ||||||
|                     id: closestId, |                     id: closestId, | ||||||
|                     type: "node", |                     type: "node", | ||||||
|                     meta: { |                     meta: { | ||||||
|                         theme: this.theme, |                         theme: this.theme, | ||||||
|                         changeType: "move" |                         changeType: "move", | ||||||
|                     }, |                     }, | ||||||
|                     changes: {lon, lat} |                     changes: { lon, lat }, | ||||||
|                 } |                 } | ||||||
|                 actualIdsToUse.push(closestId) |                 actualIdsToUse.push(closestId) | ||||||
|                 allChanges.push(change) |                 allChanges.push(change) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         if (this.newTags !== undefined && this.newTags.length > 0) { |         if (this.newTags !== undefined && this.newTags.length > 0) { | ||||||
|             const addExtraTags = new ChangeTagAction( |             const addExtraTags = new ChangeTagAction( | ||||||
|                 this.wayToReplaceId, |                 this.wayToReplaceId, | ||||||
|                 new And(this.newTags), |                 new And(this.newTags), | ||||||
|                 osmWay.tags, { |                 osmWay.tags, | ||||||
|  |                 { | ||||||
|                     theme: this.theme, |                     theme: this.theme, | ||||||
|                     changeType: "conflation" |                     changeType: "conflation", | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|             allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes)) |             allChanges.push(...(await addExtraTags.CreateChangeDescriptions(changes))) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const newCoordinates = [...this.targetCoordinates] |         const newCoordinates = [...this.targetCoordinates] | ||||||
|  | @ -468,13 +475,11 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|             const proj = Array.from(reprojectedNodes.values()) |             const proj = Array.from(reprojectedNodes.values()) | ||||||
|             proj.sort((a, b) => { |             proj.sort((a, b) => { | ||||||
|                 // Sort descending
 |                 // Sort descending
 | ||||||
|                 const diff = b.projectAfterIndex - a.projectAfterIndex; |                 const diff = b.projectAfterIndex - a.projectAfterIndex | ||||||
|                 if (diff !== 0) { |                 if (diff !== 0) { | ||||||
|                     return diff |                     return diff | ||||||
|                 } |                 } | ||||||
|                 return b.distance - a.distance; |                 return b.distance - a.distance | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
|             for (const reprojectedNode of proj) { |             for (const reprojectedNode of proj) { | ||||||
|  | @ -483,13 +488,20 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
|                     type: "node", |                     type: "node", | ||||||
|                     meta: { |                     meta: { | ||||||
|                         theme: this.theme, |                         theme: this.theme, | ||||||
|                         changeType: "move" |                         changeType: "move", | ||||||
|                     }, |                     }, | ||||||
|                     changes: {lon: reprojectedNode.newLon, lat: reprojectedNode.newLat} |                     changes: { lon: reprojectedNode.newLon, lat: reprojectedNode.newLat }, | ||||||
|                 } |                 } | ||||||
|                 allChanges.push(change) |                 allChanges.push(change) | ||||||
|                 actualIdsToUse.splice(reprojectedNode.projectAfterIndex + 1, 0, reprojectedNode.nodeId) |                 actualIdsToUse.splice( | ||||||
|                 newCoordinates.splice(reprojectedNode.projectAfterIndex + 1, 0, [reprojectedNode.newLon, reprojectedNode.newLat]) |                     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, |             id: osmWay.id, | ||||||
|             changes: { |             changes: { | ||||||
|                 nodes: actualIdsToUse, |                 nodes: actualIdsToUse, | ||||||
|                 coordinates: newCoordinates |                 coordinates: newCoordinates, | ||||||
|             }, |             }, | ||||||
|             meta: { |             meta: { | ||||||
|                 theme: this.theme, |                 theme: this.theme, | ||||||
|                 changeType: "conflation" |                 changeType: "conflation", | ||||||
|             } |             }, | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         // Some nodes might need to be deleted
 |         // Some nodes might need to be deleted
 | ||||||
|         if (detachedNodes.size > 0) { |         if (detachedNodes.size > 0) { | ||||||
|             detachedNodes.forEach(({hasTags, reason}, nodeId) => { |             detachedNodes.forEach(({ hasTags, reason }, nodeId) => { | ||||||
|                 const parentWays = nodeDb.GetParentWays(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) { |                 if (index < 0) { | ||||||
|                     console.error("ReplaceGeometryAction is trying to detach node " + nodeId + ", but it isn't listed as being part of way " + osmWay.id) |                     console.error( | ||||||
|                     return; |                         "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
 |                 // We detachted this node - so we unregister
 | ||||||
|                 parentWays.data.splice(index, 1) |                 parentWays.data.splice(index, 1) | ||||||
|                 parentWays.ping(); |                 parentWays.ping() | ||||||
| 
 | 
 | ||||||
|                 if (hasTags) { |                 if (hasTags) { | ||||||
|                     // Has tags: we leave this node alone
 |                     // Has tags: we leave this node alone
 | ||||||
|                     return; |                     return | ||||||
|                 } |                 } | ||||||
|                 if (parentWays.data.length != 0) { |                 if (parentWays.data.length != 0) { | ||||||
|                     // Still part of other ways: we leave this node alone!
 |                     // 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") |                 console.log("Removing node " + nodeId, "as it isn't needed anymore by any way") | ||||||
|                 allChanges.push({ |                 allChanges.push({ | ||||||
|                     meta: { |                     meta: { | ||||||
|                         theme: this.theme, |                         theme: this.theme, | ||||||
|                         changeType: "delete" |                         changeType: "delete", | ||||||
|                     }, |                     }, | ||||||
|                     doDelete: true, |                     doDelete: true, | ||||||
|                     type: "node", |                     type: "node", | ||||||
|  | @ -545,6 +561,4 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | ||||||
| 
 | 
 | ||||||
|         return allChanges |         return allChanges | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,21 +1,21 @@ | ||||||
| import {OsmObject, OsmWay} from "../OsmObject"; | import { OsmObject, OsmWay } from "../OsmObject" | ||||||
| import {Changes} from "../Changes"; | import { Changes } from "../Changes" | ||||||
| import {GeoOperations} from "../../GeoOperations"; | import { GeoOperations } from "../../GeoOperations" | ||||||
| import OsmChangeAction from "./OsmChangeAction"; | import OsmChangeAction from "./OsmChangeAction" | ||||||
| import {ChangeDescription} from "./ChangeDescription"; | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import RelationSplitHandler from "./RelationSplitHandler"; | import RelationSplitHandler from "./RelationSplitHandler" | ||||||
| 
 | 
 | ||||||
| interface SplitInfo { | interface SplitInfo { | ||||||
|     originalIndex?: number, // or negative for new elements
 |     originalIndex?: number // or negative for new elements
 | ||||||
|     lngLat: [number, number], |     lngLat: [number, number] | ||||||
|     doSplit: boolean |     doSplit: boolean | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default class SplitAction extends OsmChangeAction { | export default class SplitAction extends OsmChangeAction { | ||||||
|     private readonly wayId: string; |     private readonly wayId: string | ||||||
|     private readonly _splitPointsCoordinates: [number, number] []// lon, lat
 |     private readonly _splitPointsCoordinates: [number, number][] // lon, lat
 | ||||||
|     private _meta: { theme: string, changeType: "split" }; |     private _meta: { theme: string; changeType: "split" } | ||||||
|     private _toleranceInMeters: number; |     private _toleranceInMeters: number | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Create a changedescription for splitting a point. |      * Create a changedescription for splitting a point. | ||||||
|  | @ -25,12 +25,17 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|      * @param meta |      * @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 |      * @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) |         super(wayId, true) | ||||||
|         this.wayId = wayId; |         this.wayId = wayId | ||||||
|         this._splitPointsCoordinates = splitPointCoordinates |         this._splitPointsCoordinates = splitPointCoordinates | ||||||
|         this._toleranceInMeters = toleranceInMeters; |         this._toleranceInMeters = toleranceInMeters | ||||||
|         this._meta = {...meta, changeType: "split"}; |         this._meta = { ...meta, changeType: "split" } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] { |     private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] { | ||||||
|  | @ -47,16 +52,16 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         wayParts.push(currentPart) |         wayParts.push(currentPart) | ||||||
|         return wayParts.filter(wp => wp.length > 0) |         return wayParts.filter((wp) => wp.length > 0) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
|         const originalElement = <OsmWay>await OsmObject.DownloadObjectAsync(this.wayId) |         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
 |         // First, calculate splitpoints and remove points close to one another
 | ||||||
|         const splitInfo = this.CalculateSplitCoordinates(originalElement, this._toleranceInMeters) |         const splitInfo = this.CalculateSplitCoordinates(originalElement, this._toleranceInMeters) | ||||||
|         // Now we have a list with e.g. 
 |         // Now we have a list with e.g.
 | ||||||
|         // [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}]
 |         // [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}]
 | ||||||
| 
 | 
 | ||||||
|         // Lets change 'originalIndex' to the actual node id first (or assign a new id if needed):
 |         // Lets change 'originalIndex' to the actual node id first (or assign a new id if needed):
 | ||||||
|  | @ -64,19 +69,19 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|             if (element.originalIndex >= 0) { |             if (element.originalIndex >= 0) { | ||||||
|                 element.originalIndex = originalElement.nodes[element.originalIndex] |                 element.originalIndex = originalElement.nodes[element.originalIndex] | ||||||
|             } else { |             } else { | ||||||
|                 element.originalIndex = changes.getNewID(); |                 element.originalIndex = changes.getNewID() | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Next up is creating actual parts from this
 |         // 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!
 |         // Allright! At this point, we have our new ways!
 | ||||||
|         // Which one is the longest of them (and can keep the id)?
 |         // Which one is the longest of them (and can keep the id)?
 | ||||||
| 
 | 
 | ||||||
|         let longest = undefined; |         let longest = undefined | ||||||
|         for (const wayPart of wayParts) { |         for (const wayPart of wayParts) { | ||||||
|             if (longest === undefined) { |             if (longest === undefined) { | ||||||
|                 longest = wayPart; |                 longest = wayPart | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             if (wayPart.length > longest.length) { |             if (wayPart.length > longest.length) { | ||||||
|  | @ -88,16 +93,16 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|         // Let's create the new points as needed
 |         // Let's create the new points as needed
 | ||||||
|         for (const element of splitInfo) { |         for (const element of splitInfo) { | ||||||
|             if (element.originalIndex >= 0) { |             if (element.originalIndex >= 0) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
|             changeDescription.push({ |             changeDescription.push({ | ||||||
|                 type: "node", |                 type: "node", | ||||||
|                 id: element.originalIndex, |                 id: element.originalIndex, | ||||||
|                 changes: { |                 changes: { | ||||||
|                     lon: element.lngLat[0], |                     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[][] = [] |         const allWaysNodesInOrder: number[][] = [] | ||||||
|         // Lets create OsmWays based on them
 |         // Lets create OsmWays based on them
 | ||||||
|         for (const wayPart of wayParts) { |         for (const wayPart of wayParts) { | ||||||
| 
 |  | ||||||
|             let isOriginal = wayPart === longest |             let isOriginal = wayPart === longest | ||||||
|             if (isOriginal) { |             if (isOriginal) { | ||||||
|                 // We change the actual element!
 |                 // We change the actual element!
 | ||||||
|                 const nodeIds = wayPart.map(p => p.originalIndex) |                 const nodeIds = wayPart.map((p) => p.originalIndex) | ||||||
|                 changeDescription.push({ |                 changeDescription.push({ | ||||||
|                     type: "way", |                     type: "way", | ||||||
|                     id: originalElement.id, |                     id: originalElement.id, | ||||||
|                     changes: { |                     changes: { | ||||||
|                         coordinates: wayPart.map(p => p.lngLat), |                         coordinates: wayPart.map((p) => p.lngLat), | ||||||
|                         nodes: nodeIds |                         nodes: nodeIds, | ||||||
|                     }, |                     }, | ||||||
|                     meta: this._meta |                     meta: this._meta, | ||||||
|                 }) |                 }) | ||||||
|                 allWayIdsInOrder.push(originalElement.id) |                 allWayIdsInOrder.push(originalElement.id) | ||||||
|                 allWaysNodesInOrder.push(nodeIds) |                 allWaysNodesInOrder.push(nodeIds) | ||||||
|             } else { |             } else { | ||||||
|                 let id = changes.getNewID(); |                 let id = changes.getNewID() | ||||||
|                 // Copy the tags from the original object onto the new
 |                 // Copy the tags from the original object onto the new
 | ||||||
|                 const kv = [] |                 const kv = [] | ||||||
|                 for (const k in originalElement.tags) { |                 for (const k in originalElement.tags) { | ||||||
|  | @ -132,20 +136,20 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|                         continue |                         continue | ||||||
|                     } |                     } | ||||||
|                     if (k.startsWith("_") || k === "id") { |                     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({ |                 changeDescription.push({ | ||||||
|                     type: "way", |                     type: "way", | ||||||
|                     id: id, |                     id: id, | ||||||
|                     tags: kv, |                     tags: kv, | ||||||
|                     changes: { |                     changes: { | ||||||
|                         coordinates: wayPart.map(p => p.lngLat), |                         coordinates: wayPart.map((p) => p.lngLat), | ||||||
|                         nodes: nodeIds |                         nodes: nodeIds, | ||||||
|                     }, |                     }, | ||||||
|                     meta: this._meta |                     meta: this._meta, | ||||||
|                 }) |                 }) | ||||||
| 
 | 
 | ||||||
|                 allWayIdsInOrder.push(id) |                 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
 |         // At least, the order of the ways is identical, so we can keep the same roles
 | ||||||
|         const relations = await OsmObject.DownloadReferencingRelations(this.wayId) |         const relations = await OsmObject.DownloadReferencingRelations(this.wayId) | ||||||
|         for (const relation of relations) { |         for (const relation of relations) { | ||||||
|             const changDescrs = await new RelationSplitHandler({ |             const changDescrs = await new RelationSplitHandler( | ||||||
|                 relation: relation, |                 { | ||||||
|                 allWayIdsInOrder: allWayIdsInOrder, |                     relation: relation, | ||||||
|                 originalNodes: originalNodes, |                     allWayIdsInOrder: allWayIdsInOrder, | ||||||
|                 allWaysNodesInOrder: allWaysNodesInOrder, |                     originalNodes: originalNodes, | ||||||
|                 originalWayId: originalElement.id, |                     allWaysNodesInOrder: allWaysNodesInOrder, | ||||||
|             }, this._meta.theme).CreateChangeDescriptions(changes) |                     originalWayId: originalElement.id, | ||||||
|  |                 }, | ||||||
|  |                 this._meta.theme | ||||||
|  |             ).CreateChangeDescriptions(changes) | ||||||
|             changeDescription.push(...changDescrs) |             changeDescription.push(...changDescrs) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -180,48 +187,47 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|     private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] { |     private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] { | ||||||
|         const wayGeoJson = osmWay.asGeoJson() |         const wayGeoJson = osmWay.asGeoJson() | ||||||
|         // Should be [lon, lat][]
 |         // 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: { |         const allPoints: { | ||||||
|             // lon, lat
 |             // lon, lat
 | ||||||
|             coordinates: [number, number], |             coordinates: [number, number] | ||||||
|             isSplitPoint: boolean, |             isSplitPoint: boolean | ||||||
|             originalIndex?: number, // Original index
 |             originalIndex?: number // Original index
 | ||||||
|             dist: number, // Distance from the nearest point on the original line
 |             dist: number // Distance from the nearest point on the original line
 | ||||||
|             location: number // Distance from the start of the way
 |             location: number // Distance from the start of the way
 | ||||||
|         }[] = this._splitPointsCoordinates.map(c => { |         }[] = this._splitPointsCoordinates.map((c) => { | ||||||
|             // From the turf.js docs:
 |             // From the turf.js docs:
 | ||||||
|             // The properties object will contain three values: 
 |             // The properties object will contain three values:
 | ||||||
|             // - `index`: closest point was found on nth line part,
 |             // - `index`: closest point was found on nth line part,
 | ||||||
|             // - `dist`: distance between pt and the closest point, 
 |             // - `dist`: distance between pt and the closest point,
 | ||||||
|             // `location`: distance along the line between start and the closest point.
 |             // `location`: distance along the line between start and the closest point.
 | ||||||
|             let projected = GeoOperations.nearestPoint(wayGeoJson, c) |             let projected = GeoOperations.nearestPoint(wayGeoJson, c) | ||||||
|             // c is lon lat
 |             // c is lon lat
 | ||||||
|             return ({ |             return { | ||||||
|                 coordinates: c, |                 coordinates: c, | ||||||
|                 isSplitPoint: true, |                 isSplitPoint: true, | ||||||
|                 dist: projected.properties.dist, |                 dist: projected.properties.dist, | ||||||
|                 location: projected.properties.location |                 location: projected.properties.location, | ||||||
|             }); |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         // We have a bunch of coordinates here: [ [lon, lon], [lat, lon], ...] ...
 |         // 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
 |         // 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++) { |         for (let i = 0; i < originalPoints.length; i++) { | ||||||
|             let originalPoint = originalPoints[i]; |             let originalPoint = originalPoints[i] | ||||||
|             let projected = GeoOperations.nearestPoint(wayGeoJson, originalPoint) |             let projected = GeoOperations.nearestPoint(wayGeoJson, originalPoint) | ||||||
|             allPoints.push({ |             allPoints.push({ | ||||||
|                 coordinates: originalPoint, |                 coordinates: originalPoint, | ||||||
|                 isSplitPoint: false, |                 isSplitPoint: false, | ||||||
|                 location: projected.properties.location, |                 location: projected.properties.location, | ||||||
|                 originalIndex: i, |                 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
 |         // 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
 |         // We sort this list so that the new points are at the same location
 | ||||||
|         allPoints.sort((a, b) => a.location - b.location) |         allPoints.sort((a, b) => a.location - b.location) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         for (let i = allPoints.length - 2; i >= 1; i--) { |         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
 |             // 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) { |             if (distToNext * 1000 > toleranceInM && distToPrev * 1000 > toleranceInM) { | ||||||
|                 // Both are too far away to mark them as the split point
 |                 // Both are too far away to mark them as the split point
 | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             let closest = nextPoint |             let closest = nextPoint | ||||||
|  | @ -256,9 +262,8 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|                 // We can not split on the first or last points...
 |                 // We can not split on the first or last points...
 | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             closest.isSplitPoint = true; |             closest.isSplitPoint = true | ||||||
|             allPoints.splice(i, 1) |             allPoints.splice(i, 1) | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const splitInfo: SplitInfo[] = [] |         const splitInfo: SplitInfo[] = [] | ||||||
|  | @ -267,19 +272,17 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|         for (const p of allPoints) { |         for (const p of allPoints) { | ||||||
|             let index = p.originalIndex |             let index = p.originalIndex | ||||||
|             if (index === undefined) { |             if (index === undefined) { | ||||||
|                 index = nextId; |                 index = nextId | ||||||
|                 nextId--; |                 nextId-- | ||||||
|             } |             } | ||||||
|             const splitInfoElement = { |             const splitInfoElement = { | ||||||
|                 originalIndex: index, |                 originalIndex: index, | ||||||
|                 lngLat: p.coordinates, |                 lngLat: p.coordinates, | ||||||
|                 doSplit: p.isSplitPoint |                 doSplit: p.isSplitPoint, | ||||||
|             } |             } | ||||||
|             splitInfo.push(splitInfoElement) |             splitInfo.push(splitInfoElement) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return splitInfo |         return splitInfo | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,106 +1,110 @@ | ||||||
| import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject"; | import { OsmNode, OsmObject, OsmRelation, OsmWay } from "./OsmObject" | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants" | ||||||
| import OsmChangeAction from "./Actions/OsmChangeAction"; | import OsmChangeAction from "./Actions/OsmChangeAction" | ||||||
| import {ChangeDescription, ChangeDescriptionTools} from "./Actions/ChangeDescription"; | import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescription" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||||
| import SimpleMetaTagger from "../SimpleMetaTagger"; | import SimpleMetaTagger from "../SimpleMetaTagger" | ||||||
| import CreateNewNodeAction from "./Actions/CreateNewNodeAction"; | import CreateNewNodeAction from "./Actions/CreateNewNodeAction" | ||||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | import FeatureSource from "../FeatureSource/FeatureSource" | ||||||
| import {ElementStorage} from "../ElementStorage"; | import { ElementStorage } from "../ElementStorage" | ||||||
| import {GeoLocationPointProperties} from "../Actors/GeoLocationHandler"; | import { GeoLocationPointProperties } from "../Actors/GeoLocationHandler" | ||||||
| import {GeoOperations} from "../GeoOperations"; | import { GeoOperations } from "../GeoOperations" | ||||||
| import {ChangesetHandler, ChangesetTag} from "./ChangesetHandler"; | import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler" | ||||||
| import {OsmConnection} from "./OsmConnection"; | import { OsmConnection } from "./OsmConnection" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Handles all changes made to OSM. |  * Handles all changes made to OSM. | ||||||
|  * Needs an authenticator via OsmConnection |  * Needs an authenticator via OsmConnection | ||||||
|  */ |  */ | ||||||
| export class Changes { | export class Changes { | ||||||
| 
 |  | ||||||
|     public readonly name = "Newly added features" |     public readonly name = "Newly added features" | ||||||
|     /** |     /** | ||||||
|      * All the newly created features as featureSource + all the modified features |      * All the newly created features as featureSource + all the modified features | ||||||
|      */ |      */ | ||||||
|     public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]); |     public features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||||
|     public readonly pendingChanges: UIEventSource<ChangeDescription[]> = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", []) |     public readonly pendingChanges: UIEventSource<ChangeDescription[]> = | ||||||
|  |         LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", []) | ||||||
|     public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined) |     public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined) | ||||||
|     public readonly state: { allElements: ElementStorage; osmConnection: OsmConnection } |     public readonly state: { allElements: ElementStorage; osmConnection: OsmConnection } | ||||||
|     public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined) |     public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined) | ||||||
|      | 
 | ||||||
|     private historicalUserLocations: FeatureSource |     private historicalUserLocations: FeatureSource | ||||||
|     private _nextId: number = -1; // Newly assigned ID's are negative
 |     private _nextId: number = -1 // Newly assigned ID's are negative
 | ||||||
|     private readonly isUploading = new UIEventSource(false); |     private readonly isUploading = new UIEventSource(false) | ||||||
|     private readonly previouslyCreated: OsmObject[] = [] |     private readonly previouslyCreated: OsmObject[] = [] | ||||||
|     private readonly _leftRightSensitive: boolean; |     private readonly _leftRightSensitive: boolean | ||||||
|     private _changesetHandler: ChangesetHandler; |     private _changesetHandler: ChangesetHandler | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         state?: { |         state?: { | ||||||
|             allElements: ElementStorage, |             allElements: ElementStorage | ||||||
|             osmConnection: OsmConnection |             osmConnection: OsmConnection | ||||||
|         }, |         }, | ||||||
|         leftRightSensitive: boolean = false) { |         leftRightSensitive: boolean = false | ||||||
|         this._leftRightSensitive = leftRightSensitive; |     ) { | ||||||
|  |         this._leftRightSensitive = leftRightSensitive | ||||||
|         // We keep track of all changes just as well
 |         // We keep track of all changes just as well
 | ||||||
|         this.allChanges.setData([...this.pendingChanges.data]) |         this.allChanges.setData([...this.pendingChanges.data]) | ||||||
|         // If a pending change contains a negative ID, we save that
 |         // If a pending change contains a negative ID, we save that
 | ||||||
|         this._nextId = Math.min(-1, ...this.pendingChanges.data?.map(pch => pch.id) ?? []) |         this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id) ?? [])) | ||||||
|         this.state = state; |         this.state = state | ||||||
|         this._changesetHandler = state?.osmConnection?.CreateChangesetHandler(state.allElements, this) |         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
 |         // 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
 |         // This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static createChangesetFor(csId: string, |     static createChangesetFor( | ||||||
|                               allChanges: { |         csId: string, | ||||||
|                                           modifiedObjects: OsmObject[], |         allChanges: { | ||||||
|                                           newObjects: OsmObject[], |             modifiedObjects: OsmObject[] | ||||||
|                                           deletedObjects: OsmObject[] |             newObjects: OsmObject[] | ||||||
|                                       }): string { |             deletedObjects: OsmObject[] | ||||||
| 
 |         } | ||||||
|  |     ): string { | ||||||
|         const changedElements = allChanges.modifiedObjects ?? [] |         const changedElements = allChanges.modifiedObjects ?? [] | ||||||
|         const newElements = allChanges.newObjects ?? [] |         const newElements = allChanges.newObjects ?? [] | ||||||
|         const deletedElements = allChanges.deletedObjects ?? [] |         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) { |         if (newElements.length > 0) { | ||||||
|             changes += |             changes += | ||||||
|                 "\n<create>\n" + |                 "\n<create>\n" + | ||||||
|                 newElements.map(e => e.ChangesetXML(csId)).join("\n") + |                 newElements.map((e) => e.ChangesetXML(csId)).join("\n") + | ||||||
|                 "</create>"; |                 "</create>" | ||||||
|         } |         } | ||||||
|         if (changedElements.length > 0) { |         if (changedElements.length > 0) { | ||||||
|             changes += |             changes += | ||||||
|                 "\n<modify>\n" + |                 "\n<modify>\n" + | ||||||
|                 changedElements.map(e => e.ChangesetXML(csId)).join("\n") + |                 changedElements.map((e) => e.ChangesetXML(csId)).join("\n") + | ||||||
|                 "\n</modify>"; |                 "\n</modify>" | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (deletedElements.length > 0) { |         if (deletedElements.length > 0) { | ||||||
|             changes += |             changes += | ||||||
|                 "\n<delete>\n" + |                 "\n<delete>\n" + | ||||||
|                 deletedElements.map(e => e.ChangesetXML(csId)).join("\n") + |                 deletedElements.map((e) => e.ChangesetXML(csId)).join("\n") + | ||||||
|                 "\n</delete>" |                 "\n</delete>" | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         changes += "</osmChange>"; |         changes += "</osmChange>" | ||||||
|         return changes; |         return changes | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static GetNeededIds(changes: ChangeDescription[]) { |     private static GetNeededIds(changes: ChangeDescription[]) { | ||||||
|         return Utils.Dedup(changes.filter(c => c.id >= 0) |         return Utils.Dedup(changes.filter((c) => c.id >= 0).map((c) => c.type + "/" + c.id)) | ||||||
|             .map(c => c.type + "/" + c.id)) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns a new ID and updates the value for the next ID |      * Returns a new ID and updates the value for the next ID | ||||||
|      */ |      */ | ||||||
|     public getNewID() { |     public getNewID() { | ||||||
|         return this._nextId--; |         return this._nextId-- | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -109,64 +113,71 @@ export class Changes { | ||||||
|      */ |      */ | ||||||
|     public async flushChanges(flushreason: string = undefined): Promise<void> { |     public async flushChanges(flushreason: string = undefined): Promise<void> { | ||||||
|         if (this.pendingChanges.data.length === 0) { |         if (this.pendingChanges.data.length === 0) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         if (this.isUploading.data) { |         if (this.isUploading.data) { | ||||||
|             console.log("Is already uploading... Abort") |             console.log("Is already uploading... Abort") | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         console.log("Uploading changes due to: ", flushreason) |         console.log("Uploading changes due to: ", flushreason) | ||||||
|         this.isUploading.setData(true) |         this.isUploading.setData(true) | ||||||
|         try { |         try { | ||||||
|             const csNumber = await this.flushChangesAsync() |             const csNumber = await this.flushChangesAsync() | ||||||
|             this.isUploading.setData(false) |             this.isUploading.setData(false) | ||||||
|             console.log("Changes flushed. Your changeset is " + csNumber); |             console.log("Changes flushed. Your changeset is " + csNumber) | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             this.isUploading.setData(false) |             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> { |     public async applyAction(action: OsmChangeAction): Promise<void> { | ||||||
|         const changeDescriptions = await action.Perform(this) |         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) |         this.applyChanges(changeDescriptions) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public applyChanges(changes: ChangeDescription[]) { |     public applyChanges(changes: ChangeDescription[]) { | ||||||
|         console.log("Received changes:", changes) |         console.log("Received changes:", changes) | ||||||
|         this.pendingChanges.data.push(...changes); |         this.pendingChanges.data.push(...changes) | ||||||
|         this.pendingChanges.ping(); |         this.pendingChanges.ping() | ||||||
|         this.allChanges.data.push(...changes) |         this.allChanges.data.push(...changes) | ||||||
|         this.allChanges.ping() |         this.allChanges.ping() | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private calculateDistanceToChanges(change: OsmChangeAction, changeDescriptions: ChangeDescription[]) { |  | ||||||
| 
 | 
 | ||||||
|  |     private calculateDistanceToChanges( | ||||||
|  |         change: OsmChangeAction, | ||||||
|  |         changeDescriptions: ChangeDescription[] | ||||||
|  |     ) { | ||||||
|         const locations = this.historicalUserLocations?.features?.data |         const locations = this.historicalUserLocations?.features?.data | ||||||
|         if (locations === undefined) { |         if (locations === undefined) { | ||||||
|             // No state loaded or no locations -> we can't calculate...
 |             // No state loaded or no locations -> we can't calculate...
 | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         if (!change.trackStatistics) { |         if (!change.trackStatistics) { | ||||||
|             // Probably irrelevant, such as a new helper node
 |             // Probably irrelevant, such as a new helper node
 | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const now = new Date() |         const now = new Date() | ||||||
|         const recentLocationPoints = locations.map(ff => ff.feature) |         const recentLocationPoints = locations | ||||||
|             .filter(feat => feat.geometry.type === "Point") |             .map((ff) => ff.feature) | ||||||
|             .filter(feat => { |             .filter((feat) => feat.geometry.type === "Point") | ||||||
|                 const visitTime = new Date((<GeoLocationPointProperties><any>feat.properties).date) |             .filter((feat) => { | ||||||
|  |                 const visitTime = new Date( | ||||||
|  |                     (<GeoLocationPointProperties>(<any>feat.properties)).date | ||||||
|  |                 ) | ||||||
|                 // In seconds
 |                 // In seconds
 | ||||||
|                 const diff = (now.getTime() - visitTime.getTime()) / 1000 |                 const diff = (now.getTime() - visitTime.getTime()) / 1000 | ||||||
|                 return diff < Constants.nearbyVisitTime; |                 return diff < Constants.nearbyVisitTime | ||||||
|             }) |             }) | ||||||
|         if (recentLocationPoints.length === 0) { |         if (recentLocationPoints.length === 0) { | ||||||
|             // Probably no GPS enabled/no fix 
 |             // Probably no GPS enabled/no fix
 | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // The applicable points, contain information in their properties about location, time and GPS accuracy
 |         // 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) { |         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) { |             if (chng === undefined) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|  | @ -194,61 +208,85 @@ export class Changes { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return Math.min(...changedObjectCoordinates.map(coor => |         return Math.min( | ||||||
|             Math.min(...recentLocationPoints.map(gpsPoint => { |             ...changedObjectCoordinates.map((coor) => | ||||||
|                 const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint) |                 Math.min( | ||||||
|                 return GeoOperations.distanceBetween(coor, otherCoor) |                     ...recentLocationPoints.map((gpsPoint) => { | ||||||
|             })) |                         const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint) | ||||||
|         )) |                         return GeoOperations.distanceBetween(coor, otherCoor) | ||||||
|  |                     }) | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * UPload the selected changes to OSM. |      * UPload the selected changes to OSM. | ||||||
|      * Returns 'true' if successfull and if they can be removed |      * Returns 'true' if successfull and if they can be removed | ||||||
|      */ |      */ | ||||||
|     private async flushSelectChanges(pending: ChangeDescription[], openChangeset: UIEventSource<number>): Promise<boolean> { |     private async flushSelectChanges( | ||||||
|         const self = this; |         pending: ChangeDescription[], | ||||||
|  |         openChangeset: UIEventSource<number> | ||||||
|  |     ): Promise<boolean> { | ||||||
|  |         const self = this | ||||||
|         const neededIds = Changes.GetNeededIds(pending) |         const neededIds = Changes.GetNeededIds(pending) | ||||||
| 
 | 
 | ||||||
|         const osmObjects = Utils.NoNull(await Promise.all(neededIds.map(async id => |         const osmObjects = Utils.NoNull( | ||||||
|             OsmObject.DownloadObjectAsync(id).catch(e => { |             await Promise.all( | ||||||
|                 console.error("Could not download OSM-object", id, " dropping it from the changes ("+e+")") |                 neededIds.map(async (id) => | ||||||
|                 pending = pending.filter(ch => ch.type + "/" + ch.id !== id) |                     OsmObject.DownloadObjectAsync(id).catch((e) => { | ||||||
|                 return undefined; |                         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) { |         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) |         console.log("Got the fresh objects!", osmObjects, "pending: ", pending) | ||||||
|         if(pending.length == 0){ |         if (pending.length == 0) { | ||||||
|             console.log("No pending changes...") |             console.log("No pending changes...") | ||||||
|             return true; |             return true | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         const perType = Array.from( |         const perType = Array.from( | ||||||
|             Utils.Hist(pending.filter(descr => descr.meta.changeType !== undefined && descr.meta.changeType !== null) |             Utils.Hist( | ||||||
|                 .map(descr => descr.meta.changeType)), ([key, count]) => ( |                 pending | ||||||
|                 { |                     .filter( | ||||||
|                     key: key, |                         (descr) => | ||||||
|                     value: count, |                             descr.meta.changeType !== undefined && descr.meta.changeType !== null | ||||||
|                     aggregate: true |                     ) | ||||||
|                 })) |                     .map((descr) => descr.meta.changeType) | ||||||
|         const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined) |             ), | ||||||
|             .map(descr => ({ |             ([key, count]) => ({ | ||||||
|  |                 key: key, | ||||||
|  |                 value: count, | ||||||
|  |                 aggregate: true, | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  |         const motivations = pending | ||||||
|  |             .filter((descr) => descr.meta.specialMotivation !== undefined) | ||||||
|  |             .map((descr) => ({ | ||||||
|                 key: descr.meta.changeType + ":" + descr.type + "/" + descr.id, |                 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) |         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 |         const maxDistances = Constants.distanceToChangeObjectBins | ||||||
|         for (let i = 0; i < maxDistances.length; i++) { |         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
 |             // 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) { |             while (j < distances.length && distances[j] < maxDistance) { | ||||||
|                 perBinCount[i]++ |                 perBinCount[i]++ | ||||||
|  | @ -256,21 +294,23 @@ export class Changes { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const perBinMessage = Utils.NoNull(perBinCount.map((count, i) => { |         const perBinMessage = Utils.NoNull( | ||||||
|             if (count === 0) { |             perBinCount.map((count, i) => { | ||||||
|                 return undefined |                 if (count === 0) { | ||||||
|             } |                     return undefined | ||||||
|             const maxD = maxDistances[i] |                 } | ||||||
|             let key = `change_within_${maxD}m` |                 const maxD = maxDistances[i] | ||||||
|             if (maxD === Number.MAX_VALUE) { |                 let key = `change_within_${maxD}m` | ||||||
|                 key = `change_over_${maxDistances[i - 1]}m` |                 if (maxD === Number.MAX_VALUE) { | ||||||
|             } |                     key = `change_over_${maxDistances[i - 1]}m` | ||||||
|             return { |                 } | ||||||
|                 key, |                 return { | ||||||
|                 value: count, |                     key, | ||||||
|                 aggregate: true |                     value: count, | ||||||
|             } |                     aggregate: true, | ||||||
|         })) |                 } | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         // This method is only called with changedescriptions for this theme
 |         // This method is only called with changedescriptions for this theme
 | ||||||
|         const theme = pending[0].meta.theme |         const theme = pending[0].meta.theme | ||||||
|  | @ -279,46 +319,47 @@ export class Changes { | ||||||
|             comment += "\n\n" + this.extraComment.data |             comment += "\n\n" + this.extraComment.data | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const metatags: ChangesetTag[] = [{ |         const metatags: ChangesetTag[] = [ | ||||||
|             key: "comment", |             { | ||||||
|             value: comment |                 key: "comment", | ||||||
|         }, |                 value: comment, | ||||||
|  |             }, | ||||||
|             { |             { | ||||||
|                 key: "theme", |                 key: "theme", | ||||||
|                 value: theme |                 value: theme, | ||||||
|             }, |             }, | ||||||
|             ...perType, |             ...perType, | ||||||
|             ...motivations, |             ...motivations, | ||||||
|             ...perBinMessage |             ...perBinMessage, | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|         await this._changesetHandler.UploadChangeset( |         await this._changesetHandler.UploadChangeset( | ||||||
|             (csId, remappings) =>{ |             (csId, remappings) => { | ||||||
|                 if(remappings.size > 0){ |                 if (remappings.size > 0) { | ||||||
|                     console.log("Rewriting pending changes from", pending, "with", remappings) |                     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) |                     console.log("Result is", pending) | ||||||
|                 } |                 } | ||||||
|                 const changes: { |                 const changes: { | ||||||
|                     newObjects: OsmObject[], |                     newObjects: OsmObject[] | ||||||
|                     modifiedObjects: OsmObject[] |                     modifiedObjects: OsmObject[] | ||||||
|                     deletedObjects: OsmObject[] |                     deletedObjects: OsmObject[] | ||||||
|                 } = self.CreateChangesetObjects(pending, osmObjects) |                 } = self.CreateChangesetObjects(pending, osmObjects) | ||||||
|                return Changes.createChangesetFor("" + csId, changes) |                 return Changes.createChangesetFor("" + csId, changes) | ||||||
|             }, |             }, | ||||||
|             metatags, |             metatags, | ||||||
|             openChangeset |             openChangeset | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         console.log("Upload successfull!") |         console.log("Upload successfull!") | ||||||
|         return true; |         return true | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async flushChangesAsync(): Promise<void> { |     private async flushChangesAsync(): Promise<void> { | ||||||
|         const self = this; |         const self = this | ||||||
|         try { |         try { | ||||||
|             // At last, we build the changeset and upload
 |             // At last, we build the changeset and upload
 | ||||||
|             const pending = self.pendingChanges.data; |             const pending = self.pendingChanges.data | ||||||
| 
 | 
 | ||||||
|             const pendingPerTheme = new Map<string, ChangeDescription[]>() |             const pendingPerTheme = new Map<string, ChangeDescription[]>() | ||||||
|             for (const changeDescription of pending) { |             for (const changeDescription of pending) { | ||||||
|  | @ -329,50 +370,62 @@ export class Changes { | ||||||
|                 pendingPerTheme.get(theme).push(changeDescription) |                 pendingPerTheme.get(theme).push(changeDescription) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const successes = await Promise.all(Array.from(pendingPerTheme, |             const successes = await Promise.all( | ||||||
|                 async ([theme, pendingChanges]) => { |                 Array.from(pendingPerTheme, async ([theme, pendingChanges]) => { | ||||||
|                     try { |                     try { | ||||||
|                         const openChangeset = this.state.osmConnection.GetPreference("current-open-changeset-" + theme).sync( |                         const openChangeset = this.state.osmConnection | ||||||
|                             str => { |                             .GetPreference("current-open-changeset-" + theme) | ||||||
|                                 const n = Number(str); |                             .sync( | ||||||
|                                 if (isNaN(n)) { |                                 (str) => { | ||||||
|                                     return undefined |                                     const n = Number(str) | ||||||
|                                 } |                                     if (isNaN(n)) { | ||||||
|                                 return n |                                         return undefined | ||||||
|                             }, [], n => "" + n |                                     } | ||||||
|                         ); |                                     return 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) { |                     } catch (e) { | ||||||
|                         console.error("Could not upload some changes:", e) |                         console.error("Could not upload some changes:", e) | ||||||
|                         return false |                         return false | ||||||
|                     } |                     } | ||||||
|                 })) |                 }) | ||||||
|  |             ) | ||||||
| 
 | 
 | ||||||
|             if (!successes.some(s => s == false)) { |             if (!successes.some((s) => s == false)) { | ||||||
|                 // All changes successfull, we clear the data!
 |                 // All changes successfull, we clear the data!
 | ||||||
|                 this.pendingChanges.setData([]); |                 this.pendingChanges.setData([]) | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|         } catch (e) { |         } 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([]) |             self.pendingChanges.setData([]) | ||||||
|         } finally { |         } finally { | ||||||
|             self.isUploading.setData(false) |             self.isUploading.setData(false) | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): { |     public CreateChangesetObjects( | ||||||
|         newObjects: OsmObject[], |         changes: ChangeDescription[], | ||||||
|  |         downloadedOsmObjects: OsmObject[] | ||||||
|  |     ): { | ||||||
|  |         newObjects: OsmObject[] | ||||||
|         modifiedObjects: OsmObject[] |         modifiedObjects: OsmObject[] | ||||||
|         deletedObjects: OsmObject[] |         deletedObjects: OsmObject[] | ||||||
| 
 |  | ||||||
|     } { |     } { | ||||||
|         const objects: Map<string, OsmObject> = new Map<string, 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) { |         for (const o of downloadedOsmObjects) { | ||||||
|             objects.set(o.type + "/" + o.id, o) |             objects.set(o.type + "/" + o.id, o) | ||||||
|  | @ -385,7 +438,7 @@ export class Changes { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         for (const change of changes) { |         for (const change of changes) { | ||||||
|             let changed = false; |             let changed = false | ||||||
|             const id = change.type + "/" + change.id |             const id = change.type + "/" + change.id | ||||||
|             if (!objects.has(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
 |                 // 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
 |                 // This is a new object that should be created
 | ||||||
|                 states.set(id, "created") |                 states.set(id, "created") | ||||||
|                 console.log("Creating object for changeDescription", change) |                 console.log("Creating object for changeDescription", change) | ||||||
|                 let osmObj: OsmObject = undefined; |                 let osmObj: OsmObject = undefined | ||||||
|                 switch (change.type) { |                 switch (change.type) { | ||||||
|                     case "node": |                     case "node": | ||||||
|                         const n = new OsmNode(change.id) |                         const n = new OsmNode(change.id) | ||||||
|                         n.lat = change.changes["lat"] |                         n.lat = change.changes["lat"] | ||||||
|                         n.lon = change.changes["lon"] |                         n.lon = change.changes["lon"] | ||||||
|                         osmObj = n |                         osmObj = n | ||||||
|                         break; |                         break | ||||||
|                     case "way": |                     case "way": | ||||||
|                         const w = new OsmWay(change.id) |                         const w = new OsmWay(change.id) | ||||||
|                         w.nodes = change.changes["nodes"] |                         w.nodes = change.changes["nodes"] | ||||||
|                         osmObj = w |                         osmObj = w | ||||||
|                         break; |                         break | ||||||
|                     case "relation": |                     case "relation": | ||||||
|                         const r = new OsmRelation(change.id) |                         const r = new OsmRelation(change.id) | ||||||
|                         r.members = change.changes["members"] |                         r.members = change.changes["members"] | ||||||
|                         osmObj = r |                         osmObj = r | ||||||
|                         break; |                         break | ||||||
|                 } |                 } | ||||||
|                 if (osmObj === undefined) { |                 if (osmObj === undefined) { | ||||||
|                     throw "Hmm? This is a bug" |                     throw "Hmm? This is a bug" | ||||||
|  | @ -442,55 +495,57 @@ export class Changes { | ||||||
|                 let v = kv.v |                 let v = kv.v | ||||||
| 
 | 
 | ||||||
|                 if (v === "") { |                 if (v === "") { | ||||||
|                     v = undefined; |                     v = undefined | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 const oldV = obj.tags[k] |                 const oldV = obj.tags[k] | ||||||
|                 if (oldV === v) { |                 if (oldV === v) { | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 obj.tags[k] = v; |                 obj.tags[k] = v | ||||||
|                 changed = true; |                 changed = true | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (change.changes !== undefined) { |             if (change.changes !== undefined) { | ||||||
|                 switch (change.type) { |                 switch (change.type) { | ||||||
|                     case "node": |                     case "node": | ||||||
|                         // @ts-ignore
 |                         // @ts-ignore
 | ||||||
|                         const nlat = change.changes.lat; |                         const nlat = change.changes.lat | ||||||
|                         // @ts-ignore
 |                         // @ts-ignore
 | ||||||
|                         const nlon = change.changes.lon; |                         const nlon = change.changes.lon | ||||||
|                         const n = <OsmNode>obj |                         const n = <OsmNode>obj | ||||||
|                         if (n.lat !== nlat || n.lon !== nlon) { |                         if (n.lat !== nlat || n.lon !== nlon) { | ||||||
|                             n.lat = nlat; |                             n.lat = nlat | ||||||
|                             n.lon = nlon; |                             n.lon = nlon | ||||||
|                             changed = true; |                             changed = true | ||||||
|                         } |                         } | ||||||
|                         break; |                         break | ||||||
|                     case "way": |                     case "way": | ||||||
|                         const nnodes = change.changes["nodes"] |                         const nnodes = change.changes["nodes"] | ||||||
|                         const w = <OsmWay>obj |                         const w = <OsmWay>obj | ||||||
|                         if (!Utils.Identical(nnodes, w.nodes)) { |                         if (!Utils.Identical(nnodes, w.nodes)) { | ||||||
|                             w.nodes = nnodes |                             w.nodes = nnodes | ||||||
|                             changed = true; |                             changed = true | ||||||
|                         } |                         } | ||||||
|                         break; |                         break | ||||||
|                     case "relation": |                     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 |                         const r = <OsmRelation>obj | ||||||
|                         if (!Utils.Identical(nmembers, r.members, (a, b) => { |                         if ( | ||||||
|                             return a.role === b.role && a.type === b.type && a.ref === b.ref |                             !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") { |             if (changed && states.get(id) === "unchanged") { | ||||||
|  | @ -498,15 +553,13 @@ export class Changes { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         const result = { |         const result = { | ||||||
|             newObjects: [], |             newObjects: [], | ||||||
|             modifiedObjects: [], |             modifiedObjects: [], | ||||||
|             deletedObjects: [] |             deletedObjects: [], | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         objects.forEach((v, id) => { |         objects.forEach((v, id) => { | ||||||
| 
 |  | ||||||
|             const state = states.get(id) |             const state = states.get(id) | ||||||
|             if (state === "created") { |             if (state === "created") { | ||||||
|                 result.newObjects.push(v) |                 result.newObjects.push(v) | ||||||
|  | @ -517,14 +570,21 @@ export class Changes { | ||||||
|             if (state === "deleted") { |             if (state === "deleted") { | ||||||
|                 result.deletedObjects.push(v) |                 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 |         return result | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     public setHistoricalUserLocations(locations: FeatureSource ){ |     public setHistoricalUserLocations(locations: FeatureSource) { | ||||||
|         this.historicalUserLocations = locations |         this.historicalUserLocations = locations | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,28 +1,26 @@ | ||||||
| import escapeHtml from "escape-html"; | import escapeHtml from "escape-html" | ||||||
| import UserDetails, {OsmConnection} from "./OsmConnection"; | import UserDetails, { OsmConnection } from "./OsmConnection" | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import {ElementStorage} from "../ElementStorage"; | import { ElementStorage } from "../ElementStorage" | ||||||
| import Locale from "../../UI/i18n/Locale"; | import Locale from "../../UI/i18n/Locale" | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants" | ||||||
| import {Changes} from "./Changes"; | import { Changes } from "./Changes" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| export interface ChangesetTag { | export interface ChangesetTag { | ||||||
|     key: string, |     key: string | ||||||
|     value: string | number, |     value: string | number | ||||||
|     aggregate?: boolean |     aggregate?: boolean | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class ChangesetHandler { | export class ChangesetHandler { | ||||||
| 
 |     private readonly allElements: ElementStorage | ||||||
|     private readonly allElements: ElementStorage; |     private osmConnection: OsmConnection | ||||||
|     private osmConnection: OsmConnection; |     private readonly changes: Changes | ||||||
|     private readonly changes: Changes; |     private readonly _dryRun: UIEventSource<boolean> | ||||||
|     private readonly _dryRun: UIEventSource<boolean>; |     private readonly userDetails: UIEventSource<UserDetails> | ||||||
|     private readonly userDetails: UIEventSource<UserDetails>; |     private readonly auth: any | ||||||
|     private readonly auth: any; |     private readonly backend: string | ||||||
|     private readonly backend: string; |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Contains previously rewritten IDs |      * Contains previously rewritten IDs | ||||||
|  | @ -30,7 +28,6 @@ export class ChangesetHandler { | ||||||
|      */ |      */ | ||||||
|     private readonly _remappings = new Map<string, string>() |     private readonly _remappings = new Map<string, string>() | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Use 'osmConnection.CreateChangesetHandler' instead |      * Use 'osmConnection.CreateChangesetHandler' instead | ||||||
|      * @param dryRun |      * @param dryRun | ||||||
|  | @ -39,36 +36,36 @@ export class ChangesetHandler { | ||||||
|      * @param changes |      * @param changes | ||||||
|      * @param auth |      * @param auth | ||||||
|      */ |      */ | ||||||
|     constructor(dryRun: UIEventSource<boolean>, |     constructor( | ||||||
|                 osmConnection: OsmConnection, |         dryRun: UIEventSource<boolean>, | ||||||
|                 allElements: ElementStorage, |         osmConnection: OsmConnection, | ||||||
|                 changes: Changes, |         allElements: ElementStorage, | ||||||
|                 auth) { |         changes: Changes, | ||||||
|         this.osmConnection = osmConnection; |         auth | ||||||
|         this.allElements = allElements; |     ) { | ||||||
|         this.changes = changes; |         this.osmConnection = osmConnection | ||||||
|         this._dryRun = dryRun; |         this.allElements = allElements | ||||||
|         this.userDetails = osmConnection.userDetails; |         this.changes = changes | ||||||
|  |         this._dryRun = dryRun | ||||||
|  |         this.userDetails = osmConnection.userDetails | ||||||
|         this.backend = osmConnection._oauth_config.url |         this.backend = osmConnection._oauth_config.url | ||||||
|         this.auth = auth; |         this.auth = auth | ||||||
| 
 | 
 | ||||||
|         if (dryRun) { |         if (dryRun) { | ||||||
|             console.log("DRYRUN ENABLED"); |             console.log("DRYRUN ENABLED") | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|      |  | ||||||
|     /** |     /** | ||||||
|      * Creates a new list which contains every key at most once |      * 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"}]
 |      * 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[]{ |     public static removeDuplicateMetaTags(extraMetaTags: ChangesetTag[]): ChangesetTag[] { | ||||||
|         const r : ChangesetTag[] = [] |         const r: ChangesetTag[] = [] | ||||||
|         const seen = new Set<string>() |         const seen = new Set<string>() | ||||||
|         for (const extraMetaTag of extraMetaTags) { |         for (const extraMetaTag of extraMetaTags) { | ||||||
|             if(seen.has(extraMetaTag.key)){ |             if (seen.has(extraMetaTag.key)) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             r.push(extraMetaTag) |             r.push(extraMetaTag) | ||||||
|  | @ -86,7 +83,7 @@ export class ChangesetHandler { | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     static rewriteMetaTags(extraMetaTags: ChangesetTag[], rewriteIds: Map<string, string>) { |     static rewriteMetaTags(extraMetaTags: ChangesetTag[], rewriteIds: Map<string, string>) { | ||||||
|         let hasChange = false; |         let hasChange = false | ||||||
|         for (const tag of extraMetaTags) { |         for (const tag of extraMetaTags) { | ||||||
|             const match = tag.key.match(/^([a-zA-Z0-9_]+):(node\/-[0-9])$/) |             const match = tag.key.match(/^([a-zA-Z0-9_]+):(node\/-[0-9])$/) | ||||||
|             if (match == null) { |             if (match == null) { | ||||||
|  | @ -115,40 +112,48 @@ export class ChangesetHandler { | ||||||
|     public async UploadChangeset( |     public async UploadChangeset( | ||||||
|         generateChangeXML: (csid: number, remappings: Map<string, string>) => string, |         generateChangeXML: (csid: number, remappings: Map<string, string>) => string, | ||||||
|         extraMetaTags: ChangesetTag[], |         extraMetaTags: ChangesetTag[], | ||||||
|         openChangeset: UIEventSource<number>): Promise<void> { |         openChangeset: UIEventSource<number> | ||||||
| 
 |     ): Promise<void> { | ||||||
|         if (!extraMetaTags.some(tag => tag.key === "comment") || !extraMetaTags.some(tag => tag.key === "theme")) { |         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`" |             throw "The meta tags should at least contain a `comment` and a `theme`" | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         extraMetaTags = [...extraMetaTags, ...this.defaultChangesetTags()] |         extraMetaTags = [...extraMetaTags, ...this.defaultChangesetTags()] | ||||||
|         extraMetaTags = ChangesetHandler.removeDuplicateMetaTags(extraMetaTags) |         extraMetaTags = ChangesetHandler.removeDuplicateMetaTags(extraMetaTags) | ||||||
|         if (this.userDetails.data.csCount == 0) { |         if (this.userDetails.data.csCount == 0) { | ||||||
|             // The user became a contributor!
 |             // The user became a contributor!
 | ||||||
|             this.userDetails.data.csCount = 1; |             this.userDetails.data.csCount = 1 | ||||||
|             this.userDetails.ping(); |             this.userDetails.ping() | ||||||
|         } |         } | ||||||
|         if (this._dryRun.data) { |         if (this._dryRun.data) { | ||||||
|             const changesetXML = generateChangeXML(123456, this._remappings); |             const changesetXML = generateChangeXML(123456, this._remappings) | ||||||
|             console.log("Metatags are", extraMetaTags) |             console.log("Metatags are", extraMetaTags) | ||||||
|             console.log(changesetXML); |             console.log(changesetXML) | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (openChangeset.data === undefined) { |         if (openChangeset.data === undefined) { | ||||||
|             // We have to open a new changeset
 |             // We have to open a new changeset
 | ||||||
|             try { |             try { | ||||||
|                 const csId = await this.OpenChangeset(extraMetaTags) |                 const csId = await this.OpenChangeset(extraMetaTags) | ||||||
|                 openChangeset.setData(csId); |                 openChangeset.setData(csId) | ||||||
|                 const changeset = generateChangeXML(csId, this._remappings); |                 const changeset = generateChangeXML(csId, this._remappings) | ||||||
|                 console.trace("Opened a new changeset (openChangeset.data is undefined):", changeset); |                 console.trace( | ||||||
|  |                     "Opened a new changeset (openChangeset.data is undefined):", | ||||||
|  |                     changeset | ||||||
|  |                 ) | ||||||
|                 const changes = await this.UploadChange(csId, changeset) |                 const changes = await this.UploadChange(csId, changeset) | ||||||
|                 const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(extraMetaTags, changes) |                 const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags( | ||||||
|                 if(hasSpecialMotivationChanges){ |                     extraMetaTags, | ||||||
|  |                     changes | ||||||
|  |                 ) | ||||||
|  |                 if (hasSpecialMotivationChanges) { | ||||||
|                     // At this point, 'extraMetaTags' will have changed - we need to set the tags again
 |                     // At this point, 'extraMetaTags' will have changed - we need to set the tags again
 | ||||||
|                     this.UpdateTags(csId, extraMetaTags) |                     this.UpdateTags(csId, extraMetaTags) | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 console.error("Could not open/upload changeset due to ", e) |                 console.error("Could not open/upload changeset due to ", e) | ||||||
|                 openChangeset.setData(undefined) |                 openChangeset.setData(undefined) | ||||||
|  | @ -156,29 +161,32 @@ export class ChangesetHandler { | ||||||
|         } else { |         } else { | ||||||
|             // There still exists an open changeset (or at least we hope so)
 |             // There still exists an open changeset (or at least we hope so)
 | ||||||
|             // Let's check!
 |             // Let's check!
 | ||||||
|             const csId = openChangeset.data; |             const csId = openChangeset.data | ||||||
|             try { |             try { | ||||||
| 
 |  | ||||||
|                 const oldChangesetMeta = await this.GetChangesetMeta(csId) |                 const oldChangesetMeta = await this.GetChangesetMeta(csId) | ||||||
|                 if (!oldChangesetMeta.open) { |                 if (!oldChangesetMeta.open) { | ||||||
|                     // Mark the CS as closed...
 |                     // Mark the CS as closed...
 | ||||||
|                     console.log("Could not fetch the metadata from the already open changeset") |                     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  
 |                     // ... and try again. As the cs is closed, no recursive loop can exist
 | ||||||
|                     await this.UploadChangeset(generateChangeXML, extraMetaTags, openChangeset) |                     await this.UploadChangeset(generateChangeXML, extraMetaTags, openChangeset) | ||||||
|                     return; |                     return | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 const rewritings = await this.UploadChange( |                 const rewritings = await this.UploadChange( | ||||||
|                     csId, |                     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) |                 await this.UpdateTags(csId, rewrittenTags) | ||||||
| 
 |  | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 console.warn("Could not upload, changeset is probably closed: ", e); |                 console.warn("Could not upload, changeset is probably closed: ", e) | ||||||
|                 openChangeset.setData(undefined); |                 openChangeset.setData(undefined) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -190,17 +198,17 @@ export class ChangesetHandler { | ||||||
|      * @param rewriteIds: the mapping of ids |      * @param rewriteIds: the mapping of ids | ||||||
|      * @param oldChangesetMeta: the metadata-object of the already existing changeset |      * @param oldChangesetMeta: the metadata-object of the already existing changeset | ||||||
|      */ |      */ | ||||||
|     public RewriteTagsOf(extraMetaTags: ChangesetTag[], |     public RewriteTagsOf( | ||||||
|                                 rewriteIds: Map<string, string>, |         extraMetaTags: ChangesetTag[], | ||||||
|                                 oldChangesetMeta: { |         rewriteIds: Map<string, string>, | ||||||
|                                     open: boolean, |         oldChangesetMeta: { | ||||||
|                                     id: number |             open: boolean | ||||||
|                                     uid: number, // User ID
 |             id: number | ||||||
|                                     changes_count: number, |             uid: number // User ID
 | ||||||
|                                     tags: any |             changes_count: number | ||||||
|                                 }) : ChangesetTag[] { |             tags: any | ||||||
| 
 |         } | ||||||
| 
 |     ): ChangesetTag[] { | ||||||
|         // Note: extraMetaTags is where all the tags are collected into
 |         // Note: extraMetaTags is where all the tags are collected into
 | ||||||
| 
 | 
 | ||||||
|         // same as 'extraMetaTag', but indexed
 |         // same as 'extraMetaTag', but indexed
 | ||||||
|  | @ -221,7 +229,7 @@ export class ChangesetHandler { | ||||||
|             if (newMetaTag === undefined) { |             if (newMetaTag === undefined) { | ||||||
|                 extraMetaTags.push({ |                 extraMetaTags.push({ | ||||||
|                     key: key, |                     key: key, | ||||||
|                     value: oldCsTags[key] |                     value: oldCsTags[key], | ||||||
|                 }) |                 }) | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|  | @ -242,10 +250,8 @@ export class ChangesetHandler { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         ChangesetHandler.rewriteMetaTags(extraMetaTags, rewriteIds) |         ChangesetHandler.rewriteMetaTags(extraMetaTags, rewriteIds) | ||||||
|         return extraMetaTags |         return extraMetaTags | ||||||
|      |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -255,28 +261,28 @@ export class ChangesetHandler { | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private static parseIdRewrite(node: any, type: string): [string, string] { |     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) { |         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
 |         // The actual mapping
 | ||||||
|         const result: [string, string] = [type + "/" + oldId, type + "/" + newId] |         const result: [string, string] = [type + "/" + oldId, type + "/" + newId] | ||||||
|         if(oldId === newId){ |         if (oldId === newId) { | ||||||
|             return undefined; |             return undefined | ||||||
|         } |         } | ||||||
|         return result; |         return result | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Given a diff-result XML of the form  |      * Given a diff-result XML of the form | ||||||
|      * <diffResult version="0.6"> |      * <diffResult version="0.6"> | ||||||
|      *  <node old_id="-1" new_id="9650458521" new_version="1"/> |      *  <node old_id="-1" new_id="9650458521" new_version="1"/> | ||||||
|      *  <way old_id="-2" new_id="1050127772" new_version="1"/> |      *  <way old_id="-2" new_id="1050127772" new_version="1"/> | ||||||
|      * </diffResult>, |      * </diffResult>, | ||||||
|      * will: |      * will: | ||||||
|      *  |      * | ||||||
|      * - create a mapping `{'node/-1' --> "node/9650458521", 'way/-2' --> "way/9650458521"}
 |      * - create a mapping `{'node/-1' --> "node/9650458521", 'way/-2' --> "way/9650458521"}
 | ||||||
|      * - Call this.changes.registerIdRewrites |      * - Call this.changes.registerIdRewrites | ||||||
|      * - Call handleIdRewrites as needed |      * - Call handleIdRewrites as needed | ||||||
|  | @ -284,9 +290,9 @@ export class ChangesetHandler { | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private parseUploadChangesetResponse(response: XMLDocument): Map<string, string> { |     private parseUploadChangesetResponse(response: XMLDocument): Map<string, string> { | ||||||
|         const nodes = response.getElementsByTagName("node"); |         const nodes = response.getElementsByTagName("node") | ||||||
|         const mappings : [string, string][]= [] |         const mappings: [string, string][] = [] | ||||||
|          | 
 | ||||||
|         for (const node of Array.from(nodes)) { |         for (const node of Array.from(nodes)) { | ||||||
|             const mapping = ChangesetHandler.parseIdRewrite(node, "node") |             const mapping = ChangesetHandler.parseIdRewrite(node, "node") | ||||||
|             if (mapping !== undefined) { |             if (mapping !== undefined) { | ||||||
|  | @ -294,7 +300,7 @@ export class ChangesetHandler { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const ways = response.getElementsByTagName("way"); |         const ways = response.getElementsByTagName("way") | ||||||
|         for (const way of Array.from(ways)) { |         for (const way of Array.from(ways)) { | ||||||
|             const mapping = ChangesetHandler.parseIdRewrite(way, "way") |             const mapping = ChangesetHandler.parseIdRewrite(way, "way") | ||||||
|             if (mapping !== undefined) { |             if (mapping !== undefined) { | ||||||
|  | @ -303,40 +309,41 @@ export class ChangesetHandler { | ||||||
|         } |         } | ||||||
|         for (const mapping of mappings) { |         for (const mapping of mappings) { | ||||||
|             const [oldId, newId] = mapping |             const [oldId, newId] = mapping | ||||||
|             this.allElements.addAlias(oldId, newId); |             this.allElements.addAlias(oldId, newId) | ||||||
|             if(newId !== undefined) { |             if (newId !== undefined) { | ||||||
|                 this._remappings.set(mapping[0], mapping[1]) |                 this._remappings.set(mapping[0], mapping[1]) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return new Map<string, string>(mappings) |         return new Map<string, string>(mappings) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async CloseChangeset(changesetId: number = undefined): Promise<void> { |     private async CloseChangeset(changesetId: number = undefined): Promise<void> { | ||||||
|         const self = this |         const self = this | ||||||
|         return new Promise<void>(function (resolve, reject) { |         return new Promise<void>(function (resolve, reject) { | ||||||
|             if (changesetId === undefined) { |             if (changesetId === undefined) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             self.auth.xhr({ |             self.auth.xhr( | ||||||
|                 method: 'PUT', |                 { | ||||||
|                 path: '/api/0.6/changeset/' + changesetId + '/close', |                     method: "PUT", | ||||||
|             }, function (err, response) { |                     path: "/api/0.6/changeset/" + changesetId + "/close", | ||||||
|                 if (response == null) { |                 }, | ||||||
| 
 |                 function (err, response) { | ||||||
|                     console.log("err", err); |                     if (response == null) { | ||||||
|  |                         console.log("err", err) | ||||||
|  |                     } | ||||||
|  |                     console.log("Closed changeset ", changesetId) | ||||||
|  |                     resolve() | ||||||
|                 } |                 } | ||||||
|                 console.log("Closed changeset ", changesetId) |             ) | ||||||
|                 resolve() |  | ||||||
|             }); |  | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async GetChangesetMeta(csId: number): Promise<{ |     async GetChangesetMeta(csId: number): Promise<{ | ||||||
|         id: number, |         id: number | ||||||
|         open: boolean, |         open: boolean | ||||||
|         uid: number, |         uid: number | ||||||
|         changes_count: number, |         changes_count: number | ||||||
|         tags: any |         tags: any | ||||||
|     }> { |     }> { | ||||||
|         const url = `${this.backend}/api/0.6/changeset/${csId}` |         const url = `${this.backend}/api/0.6/changeset/${csId}` | ||||||
|  | @ -344,47 +351,59 @@ export class ChangesetHandler { | ||||||
|         return csData.elements[0] |         return csData.elements[0] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Puts the specified tags onto the changesets as they are. |      * Puts the specified tags onto the changesets as they are. | ||||||
|      * This method will erase previously set tags |      * This method will erase previously set tags | ||||||
|      */ |      */ | ||||||
|     private async UpdateTags( |     private async UpdateTags(csId: number, tags: ChangesetTag[]) { | ||||||
|         csId: number, |  | ||||||
|         tags: ChangesetTag[]) { |  | ||||||
|         tags = ChangesetHandler.removeDuplicateMetaTags(tags) |         tags = ChangesetHandler.removeDuplicateMetaTags(tags) | ||||||
| 
 | 
 | ||||||
|         const self = this; |         const self = this | ||||||
|         return new Promise<string>(function (resolve, reject) { |         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 !== "") |             self.auth.xhr( | ||||||
|             const metadata = tags.map(kv => `<tag k="${kv.key}" v="${escapeHtml(kv.value)}"/>`) |                 { | ||||||
| 
 |                     method: "PUT", | ||||||
|             self.auth.xhr({ |                     path: "/api/0.6/changeset/" + csId, | ||||||
|                 method: 'PUT', |                     options: { header: { "Content-Type": "text/xml" } }, | ||||||
|                 path: '/api/0.6/changeset/' + csId, |                     content: [`<osm><changeset>`, metadata, `</changeset></osm>`].join(""), | ||||||
|                 options: {header: {'Content-Type': 'text/xml'}}, |                 }, | ||||||
|                 content: [`<osm><changeset>`, |                 function (err, response) { | ||||||
|                     metadata, |                     if (response === undefined) { | ||||||
|                     `</changeset></osm>`].join("") |                         console.error("Updating the tags of changeset " + csId + " failed:", err) | ||||||
|             }, function (err, response) { |                         reject(err) | ||||||
|                 if (response === undefined) { |                     } else { | ||||||
|                     console.error("Updating the tags of changeset "+csId+" failed:", err); |                         resolve(response) | ||||||
|                     reject(err) |                     } | ||||||
|                 } else { |  | ||||||
|                     resolve(response); |  | ||||||
|                 } |                 } | ||||||
|             }); |             ) | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     private defaultChangesetTags() : ChangesetTag[]{ |     private defaultChangesetTags(): ChangesetTag[] { | ||||||
|       return  [ ["created_by", `MapComplete ${Constants.vNumber}`], |         return [ | ||||||
|  |             ["created_by", `MapComplete ${Constants.vNumber}`], | ||||||
|             ["locale", Locale.language.data], |             ["locale", Locale.language.data], | ||||||
|             ["host", `${window.location.origin}${window.location.pathname}`], |             ["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]) => ({ |                 "source", | ||||||
|             key, value, aggretage: false |                 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 |      * @constructor | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private OpenChangeset( |     private OpenChangeset(changesetTags: ChangesetTag[]): Promise<number> { | ||||||
|         changesetTags: ChangesetTag[] |         const self = this | ||||||
|     ): Promise<number> { |  | ||||||
|         const self = this; |  | ||||||
|         return new Promise<number>(function (resolve, reject) { |         return new Promise<number>(function (resolve, reject) { | ||||||
| 
 |             const metadata = changesetTags | ||||||
|             const metadata = changesetTags.map(cstag => [cstag.key, cstag.value]) |                 .map((cstag) => [cstag.key, cstag.value]) | ||||||
|                 .filter(kv => (kv[1] ?? "") !== "") |                 .filter((kv) => (kv[1] ?? "") !== "") | ||||||
|                 .map(kv => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`) |                 .map((kv) => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`) | ||||||
|                 .join("\n") |                 .join("\n") | ||||||
| 
 | 
 | ||||||
| 
 |             self.auth.xhr( | ||||||
|             self.auth.xhr({ |                 { | ||||||
|                 method: 'PUT', |                     method: "PUT", | ||||||
|                 path: '/api/0.6/changeset/create', |                     path: "/api/0.6/changeset/create", | ||||||
|                 options: {header: {'Content-Type': 'text/xml'}}, |                     options: { header: { "Content-Type": "text/xml" } }, | ||||||
|                 content: [`<osm><changeset>`, |                     content: [`<osm><changeset>`, metadata, `</changeset></osm>`].join(""), | ||||||
|                     metadata, |                 }, | ||||||
|                     `</changeset></osm>`].join("") |                 function (err, response) { | ||||||
|             }, function (err, response) { |                     if (response === undefined) { | ||||||
|                 if (response === undefined) { |                         console.error("Opening a changeset failed:", err) | ||||||
|                     console.error("Opening a changeset failed:", err); |                         reject(err) | ||||||
|                     reject(err) |                     } else { | ||||||
|                 } else { |                         resolve(Number(response)) | ||||||
|                     resolve(Number(response)); |                     } | ||||||
|                 } |                 } | ||||||
|             }); |             ) | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Upload a changesetXML |      * Upload a changesetXML | ||||||
|      */ |      */ | ||||||
|     private UploadChange(changesetId: number, |     private UploadChange(changesetId: number, changesetXML: string): Promise<Map<string, string>> { | ||||||
|                       changesetXML: string): Promise<Map<string, string>> { |         const self = this | ||||||
|         const self = this; |  | ||||||
|         return new Promise(function (resolve, reject) { |         return new Promise(function (resolve, reject) { | ||||||
|             self.auth.xhr({ |             self.auth.xhr( | ||||||
|                 method: 'POST', |                 { | ||||||
|                 options: {header: {'Content-Type': 'text/xml'}}, |                     method: "POST", | ||||||
|                 path: '/api/0.6/changeset/' + changesetId + '/upload', |                     options: { header: { "Content-Type": "text/xml" } }, | ||||||
|                 content: changesetXML |                     path: "/api/0.6/changeset/" + changesetId + "/upload", | ||||||
|             }, function (err, response) { |                     content: changesetXML, | ||||||
|                 if (response == null) { |                 }, | ||||||
|                     console.error("Uploading an actual change failed", err); |                 function (err, response) { | ||||||
|                     reject(err); |                     if (response == null) { | ||||||
|  |                         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 State from "../../State" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import {BBox} from "../BBox"; | import { BBox } from "../BBox" | ||||||
| 
 | 
 | ||||||
| export interface GeoCodeResult { | export interface GeoCodeResult { | ||||||
|     display_name: string, |     display_name: string | ||||||
|     lat: number, lon: number, boundingbox: number[], |     lat: number | ||||||
|     osm_type: "node" | "way" | "relation", |     lon: number | ||||||
|  |     boundingbox: number[] | ||||||
|  |     osm_type: "node" | "way" | "relation" | ||||||
|     osm_id: string |     osm_id: string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class Geocoding { | 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[]> { |     static async Search(query: string): Promise<GeoCodeResult[]> { | ||||||
|         const b = State?.state?.currentBounds?.data ?? BBox.global; |         const b = State?.state?.currentBounds?.data ?? BBox.global | ||||||
|         const url = Geocoding.host + "format=json&limit=1&viewbox=" + |         const url = | ||||||
|  |             Geocoding.host + | ||||||
|  |             "format=json&limit=1&viewbox=" + | ||||||
|             `${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` + |             `${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` + | ||||||
|             "&accept-language=nl&q=" + query; |             "&accept-language=nl&q=" + | ||||||
|        return Utils.downloadJson(url) |             query | ||||||
|  |         return Utils.downloadJson(url) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,153 +1,161 @@ | ||||||
| import osmAuth from "osm-auth"; | import osmAuth from "osm-auth" | ||||||
| import {Store, Stores, UIEventSource} from "../UIEventSource"; | import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||||
| import {OsmPreferences} from "./OsmPreferences"; | import { OsmPreferences } from "./OsmPreferences" | ||||||
| import {ChangesetHandler} from "./ChangesetHandler"; | import { ChangesetHandler } from "./ChangesetHandler" | ||||||
| import {ElementStorage} from "../ElementStorage"; | import { ElementStorage } from "../ElementStorage" | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg" | ||||||
| import Img from "../../UI/Base/Img"; | import Img from "../../UI/Base/Img" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import {OsmObject} from "./OsmObject"; | import { OsmObject } from "./OsmObject" | ||||||
| import {Changes} from "./Changes"; | import { Changes } from "./Changes" | ||||||
| 
 | 
 | ||||||
| export default class UserDetails { | export default class UserDetails { | ||||||
| 
 |     public loggedIn = false | ||||||
|     public loggedIn = false; |     public name = "Not logged in" | ||||||
|     public name = "Not logged in"; |     public uid: number | ||||||
|     public uid: number; |     public csCount = 0 | ||||||
|     public csCount = 0; |     public img: string | ||||||
|     public img: string; |     public unreadMessages = 0 | ||||||
|     public unreadMessages = 0; |     public totalMessages = 0 | ||||||
|     public totalMessages = 0; |     home: { lon: number; lat: number } | ||||||
|     home: { lon: number; lat: number }; |     public backend: string | ||||||
|     public backend: string; |  | ||||||
| 
 | 
 | ||||||
|     constructor(backend: string) { |     constructor(backend: string) { | ||||||
|         this.backend = backend; |         this.backend = backend | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class OsmConnection { | export class OsmConnection { | ||||||
| 
 |  | ||||||
|     public static readonly oauth_configs = { |     public static readonly oauth_configs = { | ||||||
|         "osm": { |         osm: { | ||||||
|             oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem', |             oauth_consumer_key: "hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem", | ||||||
|             oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI', |             oauth_secret: "wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI", | ||||||
|             url: "https://www.openstreetmap.org" |             url: "https://www.openstreetmap.org", | ||||||
|         }, |         }, | ||||||
|         "osm-test": { |         "osm-test": { | ||||||
|             oauth_consumer_key: 'Zgr7EoKb93uwPv2EOFkIlf3n9NLwj5wbyfjZMhz2', |             oauth_consumer_key: "Zgr7EoKb93uwPv2EOFkIlf3n9NLwj5wbyfjZMhz2", | ||||||
|             oauth_secret: '3am1i1sykHDMZ66SGq4wI2Z7cJMKgzneCHp3nctn', |             oauth_secret: "3am1i1sykHDMZ66SGq4wI2Z7cJMKgzneCHp3nctn", | ||||||
|             url: "https://master.apis.dev.openstreetmap.org" |             url: "https://master.apis.dev.openstreetmap.org", | ||||||
|         } |         }, | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
|     public auth; |     public auth | ||||||
|     public userDetails: UIEventSource<UserDetails>; |     public userDetails: UIEventSource<UserDetails> | ||||||
|     public isLoggedIn: Store<boolean> |     public isLoggedIn: Store<boolean> | ||||||
|     public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">("not-attempted") |     public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">( | ||||||
|     public preferencesHandler: OsmPreferences; |         "not-attempted" | ||||||
|  |     ) | ||||||
|  |     public preferencesHandler: OsmPreferences | ||||||
|     public readonly _oauth_config: { |     public readonly _oauth_config: { | ||||||
|         oauth_consumer_key: string, |         oauth_consumer_key: string | ||||||
|         oauth_secret: string, |         oauth_secret: string | ||||||
|         url: string |         url: string | ||||||
|     }; |     } | ||||||
|     private readonly _dryRun: UIEventSource<boolean>; |     private readonly _dryRun: UIEventSource<boolean> | ||||||
|     private fakeUser: boolean; |     private fakeUser: boolean | ||||||
|     private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []; |     private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [] | ||||||
|     private readonly _iframeMode: Boolean | boolean; |     private readonly _iframeMode: Boolean | boolean | ||||||
|     private readonly _singlePage: boolean; |     private readonly _singlePage: boolean | ||||||
|     private isChecking = false; |     private isChecking = false | ||||||
| 
 | 
 | ||||||
|     constructor(options: { |     constructor(options: { | ||||||
|                     dryRun?: UIEventSource<boolean>, |         dryRun?: UIEventSource<boolean> | ||||||
|                     fakeUser?: false | boolean, |         fakeUser?: false | boolean | ||||||
|                     oauth_token?: UIEventSource<string>, |         oauth_token?: UIEventSource<string> | ||||||
|                     // Used to keep multiple changesets open and to write to the correct changeset
 |         // Used to keep multiple changesets open and to write to the correct changeset
 | ||||||
|                     singlePage?: boolean, |         singlePage?: boolean | ||||||
|                     osmConfiguration?: "osm" | "osm-test", |         osmConfiguration?: "osm" | "osm-test" | ||||||
|                     attemptLogin?: true | boolean |         attemptLogin?: true | boolean | ||||||
|                 } |     }) { | ||||||
|     ) { |         this.fakeUser = options.fakeUser ?? false | ||||||
|         this.fakeUser = options.fakeUser ?? false; |         this._singlePage = options.singlePage ?? true | ||||||
|         this._singlePage = options.singlePage ?? true; |         this._oauth_config = | ||||||
|         this._oauth_config = OsmConnection.oauth_configs[options.osmConfiguration ?? 'osm'] ?? OsmConnection.oauth_configs.osm; |             OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ?? | ||||||
|  |             OsmConnection.oauth_configs.osm | ||||||
|         console.debug("Using backend", this._oauth_config.url) |         console.debug("Using backend", this._oauth_config.url) | ||||||
|         OsmObject.SetBackendUrl(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) { |         if (options.fakeUser) { | ||||||
|             const ud = this.userDetails.data; |             const ud = this.userDetails.data | ||||||
|             ud.csCount = 5678 |             ud.csCount = 5678 | ||||||
|             ud.loggedIn = true; |             ud.loggedIn = true | ||||||
|             ud.unreadMessages = 0 |             ud.unreadMessages = 0 | ||||||
|             ud.name = "Fake user" |             ud.name = "Fake user" | ||||||
|             ud.totalMessages = 42; |             ud.totalMessages = 42 | ||||||
|         } |         } | ||||||
|         const self = this; |         const self = this | ||||||
|         this.isLoggedIn = this.userDetails.map(user => user.loggedIn); |         this.isLoggedIn = this.userDetails.map((user) => user.loggedIn) | ||||||
|         this.isLoggedIn.addCallback(isLoggedIn => { |         this.isLoggedIn.addCallback((isLoggedIn) => { | ||||||
|             if (self.userDetails.data.loggedIn == false && isLoggedIn == true) { |             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
 |                 // 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!
 |                 // This means someone attempted to toggle this; so we attempt to login!
 | ||||||
|                 self.AttemptLogin() |                 self.AttemptLogin() | ||||||
|             } |             } | ||||||
|         }); |         }) | ||||||
|          |  | ||||||
|         this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false); |  | ||||||
| 
 | 
 | ||||||
|         this.updateAuthObject(); |         this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false) | ||||||
| 
 | 
 | ||||||
|         this.preferencesHandler = new OsmPreferences(this.auth, this); |         this.updateAuthObject() | ||||||
|  | 
 | ||||||
|  |         this.preferencesHandler = new OsmPreferences(this.auth, this) | ||||||
| 
 | 
 | ||||||
|         if (options.oauth_token?.data !== undefined) { |         if (options.oauth_token?.data !== undefined) { | ||||||
|             console.log(options.oauth_token.data) |             console.log(options.oauth_token.data) | ||||||
|             const self = this; |             const self = this | ||||||
|             this.auth.bootstrapToken(options.oauth_token.data, |             this.auth.bootstrapToken( | ||||||
|  |                 options.oauth_token.data, | ||||||
|                 (x) => { |                 (x) => { | ||||||
|                     console.log("Called back: ", x) |                     console.log("Called back: ", x) | ||||||
|                     self.AttemptLogin(); |                     self.AttemptLogin() | ||||||
|                 }, this.auth); |                 }, | ||||||
| 
 |                 this.auth | ||||||
|             options.oauth_token.setData(undefined); |             ) | ||||||
| 
 | 
 | ||||||
|  |             options.oauth_token.setData(undefined) | ||||||
|         } |         } | ||||||
|         if (this.auth.authenticated() && (options.attemptLogin !== false)) { |         if (this.auth.authenticated() && options.attemptLogin !== false) { | ||||||
|             this.AttemptLogin(); // Also updates the user badge
 |             this.AttemptLogin() // Also updates the user badge
 | ||||||
|         } else { |         } else { | ||||||
|             console.log("Not authenticated"); |             console.log("Not authenticated") | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     public CreateChangesetHandler(allElements: ElementStorage, changes: Changes){ |     public CreateChangesetHandler(allElements: ElementStorage, changes: Changes) { | ||||||
|         return new ChangesetHandler(this._dryRun, this, allElements, changes, this.auth); |         return new ChangesetHandler(this._dryRun, this, allElements, changes, this.auth) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GetPreference(key: string, defaultValue: string = undefined, prefix: string = "mapcomplete-"): UIEventSource<string> { |     public GetPreference( | ||||||
|         return this.preferencesHandler.GetPreference(key, defaultValue, prefix); |         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> { |     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) { |     public OnLoggedIn(action: (userDetails: UserDetails) => void) { | ||||||
|         this._onLoggedIn.push(action); |         this._onLoggedIn.push(action) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public LogOut() { |     public LogOut() { | ||||||
|         this.auth.logout(); |         this.auth.logout() | ||||||
|         this.userDetails.data.loggedIn = false; |         this.userDetails.data.loggedIn = false | ||||||
|         this.userDetails.data.csCount = 0; |         this.userDetails.data.csCount = 0 | ||||||
|         this.userDetails.data.name = ""; |         this.userDetails.data.name = "" | ||||||
|         this.userDetails.ping(); |         this.userDetails.ping() | ||||||
|         console.log("Logged out") |         console.log("Logged out") | ||||||
|         this.loadingStatus.setData("not-attempted") |         this.loadingStatus.setData("not-attempted") | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     public Backend(): string { |     public Backend(): string { | ||||||
|         return this._oauth_config.url; |         return this._oauth_config.url | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public AttemptLogin() { |     public AttemptLogin() { | ||||||
|  | @ -155,76 +163,81 @@ export class OsmConnection { | ||||||
|         if (this.fakeUser) { |         if (this.fakeUser) { | ||||||
|             this.loadingStatus.setData("logged-in") |             this.loadingStatus.setData("logged-in") | ||||||
|             console.log("AttemptLogin called, but ignored as fakeUser is set") |             console.log("AttemptLogin called, but ignored as fakeUser is set") | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         const self = this; |         const self = this | ||||||
|         console.log("Trying to log in..."); |         console.log("Trying to log in...") | ||||||
|         this.updateAuthObject(); |         this.updateAuthObject() | ||||||
|         this.auth.xhr({ |         this.auth.xhr( | ||||||
|             method: 'GET', |             { | ||||||
|             path: '/api/0.6/user/details' |                 method: "GET", | ||||||
|         }, function (err, details) { |                 path: "/api/0.6/user/details", | ||||||
|             if (err != null) { |             }, | ||||||
|                 console.log(err); |             function (err, details) { | ||||||
|                 self.loadingStatus.setData("error") |                 if (err != null) { | ||||||
|                 if (err.status == 401) { |                     console.log(err) | ||||||
|                     console.log("Clearing tokens...") |                     self.loadingStatus.setData("error") | ||||||
|                     // Not authorized - our token probably got revoked
 |                     if (err.status == 401) { | ||||||
|                     // Reset all the tokens
 |                         console.log("Clearing tokens...") | ||||||
|                     const tokens = [ |                         // Not authorized - our token probably got revoked
 | ||||||
|                         "https://www.openstreetmap.orgoauth_request_token_secret", |                         // Reset all the tokens
 | ||||||
|                         "https://www.openstreetmap.orgoauth_token", |                         const tokens = [ | ||||||
|                         "https://www.openstreetmap.orgoauth_token_secret"] |                             "https://www.openstreetmap.orgoauth_request_token_secret", | ||||||
|                     tokens.forEach(token => localStorage.removeItem(token)) |                             "https://www.openstreetmap.orgoauth_token", | ||||||
|  |                             "https://www.openstreetmap.orgoauth_token_secret", | ||||||
|  |                         ] | ||||||
|  |                         tokens.forEach((token) => localStorage.removeItem(token)) | ||||||
|  |                     } | ||||||
|  |                     return | ||||||
|                 } |                 } | ||||||
|                 return; | 
 | ||||||
|  |                 if (details == null) { | ||||||
|  |                     self.loadingStatus.setData("error") | ||||||
|  |                     return | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 self.CheckForMessagesContinuously() | ||||||
|  | 
 | ||||||
|  |                 // details is an XML DOM of user details
 | ||||||
|  |                 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") | ||||||
|  |                 data.uid = Number(userInfo.getAttribute("id")) | ||||||
|  |                 data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count") | ||||||
|  | 
 | ||||||
|  |                 data.img = undefined | ||||||
|  |                 const imgEl = userInfo.getElementsByTagName("img") | ||||||
|  |                 if (imgEl !== undefined && imgEl[0] !== undefined) { | ||||||
|  |                     data.img = imgEl[0].getAttribute("href") | ||||||
|  |                 } | ||||||
|  |                 data.img = data.img ?? Img.AsData(Svg.osm_logo) | ||||||
|  | 
 | ||||||
|  |                 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 } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 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")) | ||||||
|  | 
 | ||||||
|  |                 self.userDetails.ping() | ||||||
|  |                 for (const action of self._onLoggedIn) { | ||||||
|  |                     action(self.userDetails.data) | ||||||
|  |                 } | ||||||
|  |                 self._onLoggedIn = [] | ||||||
|             } |             } | ||||||
| 
 |         ) | ||||||
|             if (details == null) { |  | ||||||
|                 self.loadingStatus.setData("error") |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             self.CheckForMessagesContinuously(); |  | ||||||
| 
 |  | ||||||
|             // details is an XML DOM of user details
 |  | ||||||
|             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'); |  | ||||||
|             data.uid = Number(userInfo.getAttribute("id")) |  | ||||||
|             data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count"); |  | ||||||
| 
 |  | ||||||
|             data.img = undefined; |  | ||||||
|             const imgEl = userInfo.getElementsByTagName("img"); |  | ||||||
|             if (imgEl !== undefined && imgEl[0] !== undefined) { |  | ||||||
|                 data.img = imgEl[0].getAttribute("href"); |  | ||||||
|             } |  | ||||||
|             data.img = data.img ?? Img.AsData(Svg.osm_logo); |  | ||||||
| 
 |  | ||||||
|             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}; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             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")); |  | ||||||
| 
 |  | ||||||
|             self.userDetails.ping(); |  | ||||||
|             for (const action of self._onLoggedIn) { |  | ||||||
|                 action(self.userDetails.data); |  | ||||||
|             } |  | ||||||
|             self._onLoggedIn = []; |  | ||||||
| 
 |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public closeNote(id: number | string, text?: string): Promise<void> { |     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) |             console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) | ||||||
|             return new Promise((ok) => { |             return new Promise((ok) => { | ||||||
|                 ok() |                 ok() | ||||||
|             }); |             }) | ||||||
|         } |         } | ||||||
|         return new Promise((ok, error) => { |         return new Promise((ok, error) => { | ||||||
|             this.auth.xhr({ |             this.auth.xhr( | ||||||
|                 method: 'POST', |                 { | ||||||
|                 path: `/api/0.6/notes/${id}/close${textSuffix}`, |                     method: "POST", | ||||||
|             }, function (err, _) { |                     path: `/api/0.6/notes/${id}/close${textSuffix}`, | ||||||
|                 if (err !== null) { |                 }, | ||||||
|                     error(err) |                 function (err, _) { | ||||||
|                 } else { |                     if (err !== null) { | ||||||
|                     ok() |                         error(err) | ||||||
|  |                     } else { | ||||||
|  |                         ok() | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             }) |             ) | ||||||
| 
 |  | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public reopenNote(id: number | string, text?: string): Promise<void> { |     public reopenNote(id: number | string, text?: string): Promise<void> { | ||||||
|  | @ -259,110 +273,118 @@ export class OsmConnection { | ||||||
|             console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) |             console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) | ||||||
|             return new Promise((ok) => { |             return new Promise((ok) => { | ||||||
|                 ok() |                 ok() | ||||||
|             }); |             }) | ||||||
|         } |         } | ||||||
|         let textSuffix = "" |         let textSuffix = "" | ||||||
|         if ((text ?? "") !== "") { |         if ((text ?? "") !== "") { | ||||||
|             textSuffix = "?text=" + encodeURIComponent(text) |             textSuffix = "?text=" + encodeURIComponent(text) | ||||||
|         } |         } | ||||||
|         return new Promise((ok, error) => { |         return new Promise((ok, error) => { | ||||||
|             this.auth.xhr({ |             this.auth.xhr( | ||||||
|                 method: 'POST', |                 { | ||||||
|                 path: `/api/0.6/notes/${id}/reopen${textSuffix}` |                     method: "POST", | ||||||
|             }, function (err, _) { |                     path: `/api/0.6/notes/${id}/reopen${textSuffix}`, | ||||||
|                 if (err !== null) { |                 }, | ||||||
|                     error(err) |                 function (err, _) { | ||||||
|                 } else { |                     if (err !== null) { | ||||||
|                     ok() |                         error(err) | ||||||
|  |                     } else { | ||||||
|  |                         ok() | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             }) |             ) | ||||||
| 
 |  | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { |     public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { | ||||||
|         if (this._dryRun.data) { |         if (this._dryRun.data) { | ||||||
|             console.warn("Dryrun enabled - not actually opening note with text ", text) |             console.warn("Dryrun enabled - not actually opening note with text ", text) | ||||||
|             return new Promise<{ id: number }>((ok) => { |             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} |  | ||||||
|         return new Promise((ok, error) => { |  | ||||||
|             auth.xhr({ |  | ||||||
|                 method: 'POST', |  | ||||||
|                 path: `/api/0.6/notes.json`, |  | ||||||
|                 options: { |  | ||||||
|                     header: |  | ||||||
|                         {'Content-Type': 'application/json'} |  | ||||||
|                 }, |  | ||||||
|                 content: JSON.stringify(content) |  | ||||||
| 
 |  | ||||||
|             }, function ( |  | ||||||
|                 err, |  | ||||||
|                 response: string) { |  | ||||||
|                 console.log("RESPONSE IS", response) |  | ||||||
|                 if (err !== null) { |  | ||||||
|                     error(err) |  | ||||||
|                 } else { |  | ||||||
|                     const parsed = JSON.parse(response) |  | ||||||
|                     const id = parsed.properties.id |  | ||||||
|                     console.log("OPENED NOTE", id) |  | ||||||
|                     ok({id}) |  | ||||||
|                 } |  | ||||||
|             }) |             }) | ||||||
| 
 |         } | ||||||
|  |         const auth = this.auth | ||||||
|  |         const content = { lat, lon, text } | ||||||
|  |         return new Promise((ok, error) => { | ||||||
|  |             auth.xhr( | ||||||
|  |                 { | ||||||
|  |                     method: "POST", | ||||||
|  |                     path: `/api/0.6/notes.json`, | ||||||
|  |                     options: { | ||||||
|  |                         header: { "Content-Type": "application/json" }, | ||||||
|  |                     }, | ||||||
|  |                     content: JSON.stringify(content), | ||||||
|  |                 }, | ||||||
|  |                 function (err, response: string) { | ||||||
|  |                     console.log("RESPONSE IS", response) | ||||||
|  |                     if (err !== null) { | ||||||
|  |                         error(err) | ||||||
|  |                     } else { | ||||||
|  |                         const parsed = JSON.parse(response) | ||||||
|  |                         const id = parsed.properties.id | ||||||
|  |                         console.log("OPENED NOTE", id) | ||||||
|  |                         ok({ id }) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     public addCommentToNote(id: number | string, text: string): Promise<void> { |     public addCommentToNote(id: number | string, text: string): Promise<void> { | ||||||
|         if (this._dryRun.data) { |         if (this._dryRun.data) { | ||||||
|             console.warn("Dryrun enabled - not actually adding comment ", text, "to  note ", id) |             console.warn("Dryrun enabled - not actually adding comment ", text, "to  note ", id) | ||||||
|             return new Promise((ok) => { |             return new Promise((ok) => { | ||||||
|                 ok() |                 ok() | ||||||
|             }); |             }) | ||||||
|         } |         } | ||||||
|         if ((text ?? "") === "") { |         if ((text ?? "") === "") { | ||||||
|             throw "Invalid text!" |             throw "Invalid text!" | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return new Promise((ok, error) => { |         return new Promise((ok, error) => { | ||||||
|             this.auth.xhr({ |             this.auth.xhr( | ||||||
|                 method: 'POST', |                 { | ||||||
|  |                     method: "POST", | ||||||
| 
 | 
 | ||||||
|                 path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}` |                     path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`, | ||||||
|             }, function (err, _) { |                 }, | ||||||
|                 if (err !== null) { |                 function (err, _) { | ||||||
|                     error(err) |                     if (err !== null) { | ||||||
|                 } else { |                         error(err) | ||||||
|                     ok() |                     } else { | ||||||
|  |                         ok() | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             }) |             ) | ||||||
| 
 |  | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private updateAuthObject() { |     private updateAuthObject() { | ||||||
|         let pwaStandAloneMode = false; |         let pwaStandAloneMode = false | ||||||
|         try { |         try { | ||||||
|             if (Utils.runningFromConsole) { |             if (Utils.runningFromConsole) { | ||||||
|                 pwaStandAloneMode = true |                 pwaStandAloneMode = true | ||||||
|             } else if (window.matchMedia('(display-mode: standalone)').matches || window.matchMedia('(display-mode: fullscreen)').matches) { |             } else if ( | ||||||
|                 pwaStandAloneMode = true; |                 window.matchMedia("(display-mode: standalone)").matches || | ||||||
|  |                 window.matchMedia("(display-mode: fullscreen)").matches | ||||||
|  |             ) { | ||||||
|  |                 pwaStandAloneMode = true | ||||||
|             } |             } | ||||||
|         } catch (e) { |         } 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...
 |         // In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
 | ||||||
|         // Same for an iframe...
 |         // Same for an iframe...
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         this.auth = new osmAuth({ |         this.auth = new osmAuth({ | ||||||
|             oauth_consumer_key: this._oauth_config.oauth_consumer_key, |             oauth_consumer_key: this._oauth_config.oauth_consumer_key, | ||||||
|             oauth_secret: this._oauth_config.oauth_secret, |             oauth_secret: this._oauth_config.oauth_secret, | ||||||
|  | @ -370,22 +392,20 @@ export class OsmConnection { | ||||||
|             landing: standalone ? undefined : window.location.href, |             landing: standalone ? undefined : window.location.href, | ||||||
|             singlepage: !standalone, |             singlepage: !standalone, | ||||||
|             auto: true, |             auto: true, | ||||||
| 
 |         }) | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private CheckForMessagesContinuously() { |     private CheckForMessagesContinuously() { | ||||||
|         const self = this; |         const self = this | ||||||
|         if (this.isChecking) { |         if (this.isChecking) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         this.isChecking = true; |         this.isChecking = true | ||||||
|         Stores.Chronic(5 * 60 * 1000).addCallback(_ => { |         Stores.Chronic(5 * 60 * 1000).addCallback((_) => { | ||||||
|             if (self.isLoggedIn.data) { |             if (self.isLoggedIn.data) { | ||||||
|                 console.log("Checking for messages") |                 console.log("Checking for messages") | ||||||
|                 self.AttemptLogin(); |                 self.AttemptLogin() | ||||||
|             } |             } | ||||||
|         }); |         }) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,33 +1,32 @@ | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import * as polygon_features from "../../assets/polygon-features.json"; | import * as polygon_features from "../../assets/polygon-features.json" | ||||||
| import {Store, UIEventSource} from "../UIEventSource"; | import { Store, UIEventSource } from "../UIEventSource" | ||||||
| import {BBox} from "../BBox"; | import { BBox } from "../BBox" | ||||||
| import * as OsmToGeoJson from "osmtogeojson"; | import * as OsmToGeoJson from "osmtogeojson" | ||||||
| import {NodeId, OsmFeature, OsmId, OsmTags, RelationId, WayId} from "../../Models/OsmFeature"; | import { NodeId, OsmFeature, OsmId, OsmTags, RelationId, WayId } from "../../Models/OsmFeature" | ||||||
| 
 | 
 | ||||||
| export abstract class OsmObject { | export abstract class OsmObject { | ||||||
| 
 |  | ||||||
|     private static defaultBackend = "https://www.openstreetmap.org/" |     private static defaultBackend = "https://www.openstreetmap.org/" | ||||||
|     protected static backendURL = OsmObject.defaultBackend; |     protected static backendURL = OsmObject.defaultBackend | ||||||
|     private static polygonFeatures = OsmObject.constructPolygonFeatures() |     private static polygonFeatures = OsmObject.constructPolygonFeatures() | ||||||
|     private static objectCache = new Map<string, UIEventSource<OsmObject>>(); |     private static objectCache = new Map<string, UIEventSource<OsmObject>>() | ||||||
|     private static historyCache = new Map<string, UIEventSource<OsmObject[]>>(); |     private static historyCache = new Map<string, UIEventSource<OsmObject[]>>() | ||||||
|     type: "node" | "way" | "relation"; |     type: "node" | "way" | "relation" | ||||||
|     id: number; |     id: number | ||||||
|     /** |     /** | ||||||
|      * The OSM tags as simple object |      * The OSM tags as simple object | ||||||
|      */ |      */ | ||||||
|     tags: OsmTags ; |     tags: OsmTags | ||||||
|     version: number; |     version: number | ||||||
|     public changed: boolean = false; |     public changed: boolean = false | ||||||
|     timestamp: Date; |     timestamp: Date | ||||||
| 
 | 
 | ||||||
|     protected constructor(type: string, id: number) { |     protected constructor(type: string, id: number) { | ||||||
|         this.id = id; |         this.id = id | ||||||
|         // @ts-ignore
 |         // @ts-ignore
 | ||||||
|         this.type = type; |         this.type = type | ||||||
|         this.tags = { |         this.tags = { | ||||||
|             id: `${this.type}/${id}` |             id: `${this.type}/${id}`, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -38,63 +37,63 @@ export abstract class OsmObject { | ||||||
|         if (!url.startsWith("http")) { |         if (!url.startsWith("http")) { | ||||||
|             throw "Backend URL must begin with http" |             throw "Backend URL must begin with http" | ||||||
|         } |         } | ||||||
|         this.backendURL = url; |         this.backendURL = url | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static DownloadObject(id: string, forceRefresh: boolean = false): Store<OsmObject> { |     public static DownloadObject(id: string, forceRefresh: boolean = false): Store<OsmObject> { | ||||||
|         let src: UIEventSource<OsmObject>; |         let src: UIEventSource<OsmObject> | ||||||
|         if (OsmObject.objectCache.has(id)) { |         if (OsmObject.objectCache.has(id)) { | ||||||
|             src = OsmObject.objectCache.get(id) |             src = OsmObject.objectCache.get(id) | ||||||
|             if (forceRefresh) { |             if (forceRefresh) { | ||||||
|                 src.setData(undefined) |                 src.setData(undefined) | ||||||
|             } else { |             } else { | ||||||
|                 return src; |                 return src | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             src = UIEventSource.FromPromise(OsmObject.DownloadObjectAsync(id)) |             src = UIEventSource.FromPromise(OsmObject.DownloadObjectAsync(id)) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         OsmObject.objectCache.set(id, src); |         OsmObject.objectCache.set(id, src) | ||||||
|         return src; |         return src | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static async DownloadPropertiesOf(id: string): Promise<any> { |     static async DownloadPropertiesOf(id: string): Promise<any> { | ||||||
|         const splitted = id.split("/"); |         const splitted = id.split("/") | ||||||
|         const idN = Number(splitted[1]); |         const idN = Number(splitted[1]) | ||||||
|         if (idN < 0) { |         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) |         const rawData = await Utils.downloadJsonCached(url, 1000) | ||||||
|         return rawData.elements[0].tags |         return rawData.elements[0].tags | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static async DownloadObjectAsync(id: NodeId): Promise<OsmNode | undefined>; |     static async DownloadObjectAsync(id: NodeId): Promise<OsmNode | undefined> | ||||||
|     static async DownloadObjectAsync(id: WayId): Promise<OsmWay | undefined>; |     static async DownloadObjectAsync(id: WayId): Promise<OsmWay | undefined> | ||||||
|     static async DownloadObjectAsync(id: RelationId): Promise<OsmRelation | undefined>; |     static async DownloadObjectAsync(id: RelationId): Promise<OsmRelation | undefined> | ||||||
|     static async DownloadObjectAsync(id: OsmId): Promise<OsmObject | 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> | ||||||
|     static async DownloadObjectAsync(id: string): Promise<OsmObject | undefined>{ |     static async DownloadObjectAsync(id: string): Promise<OsmObject | undefined> { | ||||||
|         const splitted = id.split("/"); |         const splitted = id.split("/") | ||||||
|         const type = splitted[0]; |         const type = splitted[0] | ||||||
|         const idN = Number(splitted[1]); |         const idN = Number(splitted[1]) | ||||||
|         if (idN < 0) { |         if (idN < 0) { | ||||||
|             return undefined; |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const full = (!id.startsWith("node")) ? "/full" : ""; |         const full = !id.startsWith("node") ? "/full" : "" | ||||||
|         const url = `${OsmObject.backendURL}api/0.6/${id}${full}`; |         const url = `${OsmObject.backendURL}api/0.6/${id}${full}` | ||||||
|         const rawData = await Utils.downloadJsonCached(url, 10000) |         const rawData = await Utils.downloadJsonCached(url, 10000) | ||||||
|         if (rawData === undefined) { |         if (rawData === undefined) { | ||||||
|             return 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)
 |         // 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
 |         // Lets fetch the object we need
 | ||||||
|         for (const osmObject of parsed) { |         for (const osmObject of parsed) { | ||||||
|             if (osmObject.type !== type) { |             if (osmObject.type !== type) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
|             if (osmObject.id !== idN) { |             if (osmObject.id !== idN) { | ||||||
|                 continue |                 continue | ||||||
|  | @ -103,25 +102,23 @@ export abstract class OsmObject { | ||||||
|             return osmObject |             return osmObject | ||||||
|         } |         } | ||||||
|         throw "PANIC: requested object is not part of the response" |         throw "PANIC: requested object is not part of the response" | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Downloads the ways that are using this node. |      * Downloads the ways that are using this node. | ||||||
|      * Beware: their geometry will be incomplete! |      * Beware: their geometry will be incomplete! | ||||||
|      */ |      */ | ||||||
|     public static DownloadReferencingWays(id: string): Promise<OsmWay[]> { |     public static DownloadReferencingWays(id: string): Promise<OsmWay[]> { | ||||||
|         return Utils.downloadJsonCached(`${OsmObject.backendURL}api/0.6/${id}/ways`, 60 * 1000).then( |         return Utils.downloadJsonCached( | ||||||
|             data => { |             `${OsmObject.backendURL}api/0.6/${id}/ways`, | ||||||
|                 return data.elements.map(wayInfo => { |             60 * 1000 | ||||||
|                     const way = new OsmWay(wayInfo.id) |         ).then((data) => { | ||||||
|                     way.LoadData(wayInfo) |             return data.elements.map((wayInfo) => { | ||||||
|                     return way |                 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! |      * Beware: their geometry will be incomplete! | ||||||
|      */ |      */ | ||||||
|     public static async DownloadReferencingRelations(id: string): Promise<OsmRelation[]> { |     public static async DownloadReferencingRelations(id: string): Promise<OsmRelation[]> { | ||||||
|         const data = await Utils.downloadJsonCached(`${OsmObject.backendURL}api/0.6/${id}/relations`, 60 * 1000) |         const data = await Utils.downloadJsonCached( | ||||||
|         return data.elements.map(wayInfo => { |             `${OsmObject.backendURL}api/0.6/${id}/relations`, | ||||||
|  |             60 * 1000 | ||||||
|  |         ) | ||||||
|  |         return data.elements.map((wayInfo) => { | ||||||
|             const rel = new OsmRelation(wayInfo.id) |             const rel = new OsmRelation(wayInfo.id) | ||||||
|             rel.LoadData(wayInfo) |             rel.LoadData(wayInfo) | ||||||
|             rel.SaveExtraData(wayInfo, undefined) |             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)) { |         if (OsmObject.historyCache.has(id)) { | ||||||
|             return OsmObject.historyCache.get(id) |             return OsmObject.historyCache.get(id) | ||||||
|         } |         } | ||||||
|         const splitted = id.split("/"); |         const splitted = id.split("/") | ||||||
|         const type = splitted[0]; |         const type = splitted[0] | ||||||
|         const idN = Number(splitted[1]); |         const idN = Number(splitted[1]) | ||||||
|         const src = new UIEventSource<OsmObject[]>([]); |         const src = new UIEventSource<OsmObject[]>([]) | ||||||
|         OsmObject.historyCache.set(id, src); |         OsmObject.historyCache.set(id, src) | ||||||
|         Utils.downloadJsonCached(`${OsmObject.backendURL}api/0.6/${type}/${idN}/history`, 10 * 60 * 1000).then(data => { |         Utils.downloadJsonCached( | ||||||
|             const elements: any[] = data.elements; |             `${OsmObject.backendURL}api/0.6/${type}/${idN}/history`, | ||||||
|  |             10 * 60 * 1000 | ||||||
|  |         ).then((data) => { | ||||||
|  |             const elements: any[] = data.elements | ||||||
|             const osmObjects: OsmObject[] = [] |             const osmObjects: OsmObject[] = [] | ||||||
|             for (const element of elements) { |             for (const element of elements) { | ||||||
|                 let osmObject: OsmObject = null |                 let osmObject: OsmObject = null | ||||||
|                 switch (type) { |                 switch (type) { | ||||||
|                     case("node"): |                     case "node": | ||||||
|                         osmObject = new OsmNode(idN); |                         osmObject = new OsmNode(idN) | ||||||
|                         break; |                         break | ||||||
|                     case("way"): |                     case "way": | ||||||
|                         osmObject = new OsmWay(idN); |                         osmObject = new OsmWay(idN) | ||||||
|                         break; |                         break | ||||||
|                     case("relation"): |                     case "relation": | ||||||
|                         osmObject = new OsmRelation(idN); |                         osmObject = new OsmRelation(idN) | ||||||
|                         break; |                         break | ||||||
|                 } |                 } | ||||||
|                 osmObject?.LoadData(element); |                 osmObject?.LoadData(element) | ||||||
|                 osmObject?.SaveExtraData(element, []); |                 osmObject?.SaveExtraData(element, []) | ||||||
|                 osmObjects.push(osmObject) |                 osmObjects.push(osmObject) | ||||||
|             } |             } | ||||||
|             src.setData(osmObjects) |             src.setData(osmObjects) | ||||||
|         }) |         }) | ||||||
|         return src; |         return src | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
 |     // bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
 | ||||||
|     public static async LoadArea(bbox: BBox): Promise<OsmObject[]> { |     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 url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` | ||||||
|         const data = await Utils.downloadJson(url) |         const data = await Utils.downloadJson(url) | ||||||
|         const elements: any[] = data.elements; |         const elements: any[] = data.elements | ||||||
|         return OsmObject.ParseObjects(elements); |         return OsmObject.ParseObjects(elements) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static ParseObjects(elements: any[]): OsmObject[] { |     public static ParseObjects(elements: any[]): OsmObject[] { | ||||||
|         const objects: OsmObject[] = []; |         const objects: OsmObject[] = [] | ||||||
|         const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>() |         const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>() | ||||||
| 
 | 
 | ||||||
|         for (const element of elements) { |         for (const element of elements) { | ||||||
|             const type = element.type; |             const type = element.type | ||||||
|             const idN = element.id; |             const idN = element.id | ||||||
|             let osmObject: OsmObject = null |             let osmObject: OsmObject = null | ||||||
|             switch (type) { |             switch (type) { | ||||||
|                 case("node"): |                 case "node": | ||||||
|                     const node = new OsmNode(idN); |                     const node = new OsmNode(idN) | ||||||
|                     allNodes.set(idN, node); |                     allNodes.set(idN, node) | ||||||
|                     osmObject = node |                     osmObject = node | ||||||
|                     node.SaveExtraData(element); |                     node.SaveExtraData(element) | ||||||
|                     break; |                     break | ||||||
|                 case("way"): |                 case "way": | ||||||
|                     osmObject = new OsmWay(idN); |                     osmObject = new OsmWay(idN) | ||||||
|                     const nodes = element.nodes.map(i => allNodes.get(i)); |                     const nodes = element.nodes.map((i) => allNodes.get(i)) | ||||||
|                     osmObject.SaveExtraData(element, nodes) |                     osmObject.SaveExtraData(element, nodes) | ||||||
|                     break; |                     break | ||||||
|                 case("relation"): |                 case "relation": | ||||||
|                     osmObject = new OsmRelation(idN); |                     osmObject = new OsmRelation(idN) | ||||||
|                     const allGeojsons = OsmToGeoJson.default({elements}, |                     const allGeojsons = OsmToGeoJson.default( | ||||||
|  |                         { elements }, | ||||||
|                         // @ts-ignore
 |                         // @ts-ignore
 | ||||||
|                         { |                         { | ||||||
|                             flatProperties: true |                             flatProperties: true, | ||||||
|                         }); |                         } | ||||||
|                     const feature = allGeojsons.features.find(f => f.id === osmObject.type + "/" + osmObject.id) |                     ) | ||||||
|  |                     const feature = allGeojsons.features.find( | ||||||
|  |                         (f) => f.id === osmObject.type + "/" + osmObject.id | ||||||
|  |                     ) | ||||||
|                     osmObject.SaveExtraData(element, feature) |                     osmObject.SaveExtraData(element, feature) | ||||||
|                     break; |                     break | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (osmObject !== undefined && OsmObject.backendURL !== OsmObject.defaultBackend) { |             if (osmObject !== undefined && OsmObject.backendURL !== OsmObject.defaultBackend) { | ||||||
|  | @ -219,12 +226,12 @@ export abstract class OsmObject { | ||||||
|             osmObject?.LoadData(element) |             osmObject?.LoadData(element) | ||||||
|             objects.push(osmObject) |             objects.push(osmObject) | ||||||
|         } |         } | ||||||
|         return objects; |         return objects | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Uses the list of polygon features to determine if the given tags are a polygon or not. |      * Uses the list of polygon features to determine if the given tags are a polygon or not. | ||||||
|      *  |      * | ||||||
|      * OsmObject.isPolygon({"building":"yes"}) // => true
 |      * OsmObject.isPolygon({"building":"yes"}) // => true
 | ||||||
|      * OsmObject.isPolygon({"highway":"residential"}) // => false
 |      * OsmObject.isPolygon({"highway":"residential"}) // => false
 | ||||||
|      * */ |      * */ | ||||||
|  | @ -233,11 +240,12 @@ export abstract class OsmObject { | ||||||
|             if (!tags.hasOwnProperty(tagsKey)) { |             if (!tags.hasOwnProperty(tagsKey)) { | ||||||
|                 continue |                 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) { |             if (polyGuide === undefined) { | ||||||
|                 continue |                 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)
 |                 // .values is null, thus merely _having_ this key is enough to be a polygon (or if blacklist, being a line)
 | ||||||
|                 return !polyGuide.blacklist |                 return !polyGuide.blacklist | ||||||
|             } |             } | ||||||
|  | @ -249,156 +257,178 @@ export abstract class OsmObject { | ||||||
|             return doesMatch |             return doesMatch | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return false; |         return false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static constructPolygonFeatures(): Map<string, { values: Set<string>, blacklist: boolean }> { |     private static constructPolygonFeatures(): Map< | ||||||
|         const result = new Map<string, { values: Set<string>, blacklist: boolean }>(); |         string, | ||||||
|         for (const polygonFeature of (polygon_features["default"] ?? polygon_features)) { |         { values: Set<string>; blacklist: boolean } | ||||||
|             const key = polygonFeature.key; |     > { | ||||||
|  |         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") { |             if (polygonFeature.polygon === "all") { | ||||||
|                 result.set(key, {values: null, blacklist: false}) |                 result.set(key, { values: null, blacklist: false }) | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const blacklist = polygonFeature.polygon === "blacklist" |             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]
 |     // 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 |      * Generates the changeset-XML for tags | ||||||
|      * @constructor |      * @constructor | ||||||
|      */ |      */ | ||||||
|     TagsXML(): string { |     TagsXML(): string { | ||||||
|         let tags = ""; |         let tags = "" | ||||||
|         for (const key in this.tags) { |         for (const key in this.tags) { | ||||||
|             if (key.startsWith("_")) { |             if (key.startsWith("_")) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
|             if (key === "id") { |             if (key === "id") { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
|             const v = this.tags[key]; |             const v = this.tags[key] | ||||||
|             if (v !== "" && v !== undefined) { |             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() { |     protected VersionXML() { | ||||||
|         if (this.version === undefined) { |         if (this.version === undefined) { | ||||||
|             return ""; |             return "" | ||||||
|         } |         } | ||||||
|         return 'version="' + this.version + '"'; |         return 'version="' + this.version + '"' | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private LoadData(element: any): void { |     private LoadData(element: any): void { | ||||||
|         this.tags = element.tags ?? this.tags; |         this.tags = element.tags ?? this.tags | ||||||
|         this.version = element.version; |         this.version = element.version | ||||||
|         this.timestamp = element.timestamp; |         this.timestamp = element.timestamp | ||||||
|         const tgs = this.tags; |         const tgs = this.tags | ||||||
|         if (element.tags === undefined) { |         if (element.tags === undefined) { | ||||||
|             // Simple node which is part of a way - not important
 |             // Simple node which is part of a way - not important
 | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         tgs["_last_edit:contributor"] = element.user |         tgs["_last_edit:contributor"] = element.user | ||||||
|         tgs["_last_edit:contributor:uid"] = element.uid |         tgs["_last_edit:contributor:uid"] = element.uid | ||||||
|         tgs["_last_edit:changeset"] = element.changeset |         tgs["_last_edit:changeset"] = element.changeset | ||||||
|         tgs["_last_edit:timestamp"] = element.timestamp |         tgs["_last_edit:timestamp"] = element.timestamp | ||||||
|         tgs["_version_number"] = element.version |         tgs["_version_number"] = element.version | ||||||
|         tgs["id"] =<OsmId> ( this.type + "/" + this.id); |         tgs["id"] = <OsmId>(this.type + "/" + this.id) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| export class OsmNode extends OsmObject { | export class OsmNode extends OsmObject { | ||||||
| 
 |     lat: number | ||||||
|     lat: number; |     lon: number | ||||||
|     lon: number; |  | ||||||
| 
 | 
 | ||||||
|     constructor(id: number) { |     constructor(id: number) { | ||||||
|         super("node", id); |         super("node", id) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ChangesetXML(changesetId: string): string { |     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 + |             tags + | ||||||
|             '    </node>\n'; |             "    </node>\n" | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     SaveExtraData(element) { |     SaveExtraData(element) { | ||||||
|         this.lat = element.lat; |         this.lat = element.lat | ||||||
|         this.lon = element.lon; |         this.lon = element.lon | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     centerpoint(): [number, number] { |     centerpoint(): [number, number] { | ||||||
|         return [this.lat, this.lon]; |         return [this.lat, this.lon] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asGeoJson() : OsmFeature{ |     asGeoJson(): OsmFeature { | ||||||
|         return { |         return { | ||||||
|             "type": "Feature", |             type: "Feature", | ||||||
|             "properties": this.tags, |             properties: this.tags, | ||||||
|             "geometry": { |             geometry: { | ||||||
|                 "type": "Point", |                 type: "Point", | ||||||
|                 "coordinates": [ |                 coordinates: [this.lon, this.lat], | ||||||
|                     this.lon, |             }, | ||||||
|                     this.lat |  | ||||||
|                 ] |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class OsmWay extends OsmObject { | export class OsmWay extends OsmObject { | ||||||
| 
 |     nodes: number[] = [] | ||||||
|     nodes: number[] = []; |  | ||||||
|     // The coordinates of the way, [lat, lon][]
 |     // The coordinates of the way, [lat, lon][]
 | ||||||
|     coordinates: [number, number][] = [] |     coordinates: [number, number][] = [] | ||||||
|     lat: number; |     lat: number | ||||||
|     lon: number; |     lon: number | ||||||
| 
 | 
 | ||||||
|     constructor(id: number) { |     constructor(id: number) { | ||||||
|         super("way", id); |         super("way", id) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     centerpoint(): [number, number] { |     centerpoint(): [number, number] { | ||||||
|         return [this.lat, this.lon]; |         return [this.lat, this.lon] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ChangesetXML(changesetId: string): string { |     ChangesetXML(changesetId: string): string { | ||||||
|         let tags = this.TagsXML(); |         let tags = this.TagsXML() | ||||||
|         let nds = ""; |         let nds = "" | ||||||
|         for (const node in this.nodes) { |         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 + |             nds + | ||||||
|             tags + |             tags + | ||||||
|             '    </way>\n'; |             "    </way>\n" | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     SaveExtraData(element, allNodes: OsmNode[]) { |     SaveExtraData(element, allNodes: OsmNode[]) { | ||||||
| 
 |  | ||||||
|         let latSum = 0 |         let latSum = 0 | ||||||
|         let lonSum = 0 |         let lonSum = 0 | ||||||
| 
 | 
 | ||||||
|  | @ -416,88 +446,96 @@ export class OsmWay extends OsmObject { | ||||||
|             if (node === undefined) { |             if (node === undefined) { | ||||||
|                 console.error("Error: node ", nodeId, "not found in ", nodeDict) |                 console.error("Error: node ", nodeId, "not found in ", nodeDict) | ||||||
|                 // This is probably part of a relation which hasn't been fully downloaded
 |                 // 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 |             latSum += node.lat | ||||||
|             lonSum += node.lon |             lonSum += node.lon | ||||||
|         } |         } | ||||||
|         let count = this.coordinates.length; |         let count = this.coordinates.length | ||||||
|         this.lat = latSum / count; |         this.lat = latSum / count | ||||||
|         this.lon = lonSum / count; |         this.lon = lonSum / count | ||||||
|         this.nodes = element.nodes; |         this.nodes = element.nodes | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public asGeoJson() { |     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()) { |         if (this.isPolygon()) { | ||||||
|             coordinates = [coordinates] |             coordinates = [coordinates] | ||||||
|         } |         } | ||||||
|         return { |         return { | ||||||
|             "type": "Feature", |             type: "Feature", | ||||||
|             "properties": this.tags, |             properties: this.tags, | ||||||
|             "geometry": { |             geometry: { | ||||||
|                 "type": this.isPolygon() ? "Polygon" : "LineString", |                 type: this.isPolygon() ? "Polygon" : "LineString", | ||||||
|                 "coordinates": coordinates |                 coordinates: coordinates, | ||||||
|             } |             }, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private isPolygon(): boolean { |     private isPolygon(): boolean { | ||||||
|         // Compare lat and lon seperately, as the coordinate array might not be a reference to the same object
 |         // 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] || |         if ( | ||||||
|             this.coordinates[0][1] !== this.coordinates[this.coordinates.length - 1][1]) { |             this.coordinates[0][0] !== this.coordinates[this.coordinates.length - 1][0] || | ||||||
|             return false; // Not closed
 |             this.coordinates[0][1] !== this.coordinates[this.coordinates.length - 1][1] | ||||||
|  |         ) { | ||||||
|  |             return false // Not closed
 | ||||||
|         } |         } | ||||||
|         return OsmObject.isPolygon(this.tags) |         return OsmObject.isPolygon(this.tags) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class OsmRelation extends OsmObject { | export class OsmRelation extends OsmObject { | ||||||
| 
 |  | ||||||
|     public members: { |     public members: { | ||||||
|         type: "node" | "way" | "relation", |         type: "node" | "way" | "relation" | ||||||
|         ref: number, |         ref: number | ||||||
|         role: string |         role: string | ||||||
|     }[]; |     }[] | ||||||
| 
 | 
 | ||||||
|     private geojson = undefined |     private geojson = undefined | ||||||
| 
 | 
 | ||||||
|     constructor(id: number) { |     constructor(id: number) { | ||||||
|         super("relation", id); |         super("relation", id) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     centerpoint(): [number, number] { |     centerpoint(): [number, number] { | ||||||
|         return [0, 0]; // TODO
 |         return [0, 0] // TODO
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ChangesetXML(changesetId: string): string { |     ChangesetXML(changesetId: string): string { | ||||||
|         let members = ""; |         let members = "" | ||||||
|         for (const member of this.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 = "" |         let cs = "" | ||||||
|         if (changesetId !== undefined) { |         if (changesetId !== undefined) { | ||||||
|             cs = `changeset="${changesetId}"` |             cs = `changeset="${changesetId}"` | ||||||
|         } |         } | ||||||
|         return `    <relation id="${this.id}" ${cs} ${this.VersionXML()}>
 |         return `    <relation id="${this.id}" ${cs} ${this.VersionXML()}>
 | ||||||
| ${members}${tags}        </relation> | ${members}${tags}        </relation> | ||||||
| `;
 | ` | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     SaveExtraData(element, geojson) { |     SaveExtraData(element, geojson) { | ||||||
|         this.members = element.members; |         this.members = element.members | ||||||
|         this.geojson = geojson |         this.geojson = geojson | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asGeoJson(): any { |     asGeoJson(): any { | ||||||
|         if (this.geojson !== undefined) { |         if (this.geojson !== undefined) { | ||||||
|             return this.geojson; |             return this.geojson | ||||||
|         } |         } | ||||||
|         throw "Not Implemented" |         throw "Not Implemented" | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,22 +1,21 @@ | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import UserDetails, {OsmConnection} from "./OsmConnection"; | import UserDetails, { OsmConnection } from "./OsmConnection" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import {DomEvent} from "leaflet"; | import { DomEvent } from "leaflet" | ||||||
| import preventDefault = DomEvent.preventDefault; | import preventDefault = DomEvent.preventDefault | ||||||
| 
 | 
 | ||||||
| export class OsmPreferences { | 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 readonly preferenceSources = new Map<string, UIEventSource<string>>() | ||||||
|     private auth: any; |     private auth: any | ||||||
|     private userDetails: UIEventSource<UserDetails>; |     private userDetails: UIEventSource<UserDetails> | ||||||
|     private longPreferences = {}; |     private longPreferences = {} | ||||||
| 
 | 
 | ||||||
|     constructor(auth, osmConnection: OsmConnection) { |     constructor(auth, osmConnection: OsmConnection) { | ||||||
|         this.auth = auth; |         this.auth = auth | ||||||
|         this.userDetails = osmConnection.userDetails; |         this.userDetails = osmConnection.userDetails | ||||||
|         const self = this; |         const self = this | ||||||
|         osmConnection.OnLoggedIn(() => self.UpdatePreferences()); |         osmConnection.OnLoggedIn(() => self.UpdatePreferences()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -26,42 +25,44 @@ export class OsmPreferences { | ||||||
|      * @constructor |      * @constructor | ||||||
|      */ |      */ | ||||||
|     public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { |     public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { | ||||||
| 
 |  | ||||||
|         if (this.longPreferences[prefix + key] !== undefined) { |         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); |         const allStartWith = prefix + key + "-combined" | ||||||
|         this.longPreferences[prefix + key] = source; |  | ||||||
| 
 |  | ||||||
|         const allStartWith = prefix + key + "-combined"; |  | ||||||
|         // Gives the number of combined preferences
 |         // Gives the number of combined preferences
 | ||||||
|         const length = this.GetPreference(allStartWith + "-length", "", ""); |         const length = this.GetPreference(allStartWith + "-length", "", "") | ||||||
| 
 | 
 | ||||||
|        if( (allStartWith + "-length").length > 255){ |         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" |             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; |         const self = this | ||||||
|         source.addCallback(str => { |         source.addCallback((str) => { | ||||||
|             if (str === undefined || str === "") { |             if (str === undefined || str === "") { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             if (str === null) { |             if (str === null) { | ||||||
|                 console.error("Deleting " + allStartWith); |                 console.error("Deleting " + allStartWith) | ||||||
|                 let count = parseInt(length.data); |                 let count = parseInt(length.data) | ||||||
|                 for (let i = 0; i < count; i++) { |                 for (let i = 0; i < count; i++) { | ||||||
|                     // Delete all the preferences
 |                     // Delete all the preferences
 | ||||||
|                     self.GetPreference(allStartWith + "-" + i, "", "") |                     self.GetPreference(allStartWith + "-" + i, "", "").setData("") | ||||||
|                         .setData(""); |  | ||||||
|                 } |                 } | ||||||
|                 self.GetPreference(allStartWith + "-length", "", "") |                 self.GetPreference(allStartWith + "-length", "", "").setData("") | ||||||
|                     .setData(""); |  | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             let i = 0; |             let i = 0 | ||||||
|             while (str !== "") { |             while (str !== "") { | ||||||
|                 if (str === undefined || str === "undefined") { |                 if (str === undefined || str === "undefined") { | ||||||
|                     throw "Long pref became undefined?" |                     throw "Long pref became undefined?" | ||||||
|  | @ -69,79 +70,91 @@ export class OsmPreferences { | ||||||
|                 if (i > 100) { |                 if (i > 100) { | ||||||
|                     throw "This long preference is getting very long... " |                     throw "This long preference is getting very long... " | ||||||
|                 } |                 } | ||||||
|                 self.GetPreference(allStartWith + "-" + i, "","").setData(str.substr(0, 255)); |                 self.GetPreference(allStartWith + "-" + i, "", "").setData(str.substr(0, 255)) | ||||||
|                 str = str.substr(255); |                 str = str.substr(255) | ||||||
|                 i++; |                 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){ |  | ||||||
|                 // The preferences are still empty - they are not yet updated, so we delay updating for now 
 |  | ||||||
|                 return |  | ||||||
|             } |  | ||||||
|             const prefsCount = Number(l); |  | ||||||
|             if (prefsCount > 100) { |  | ||||||
|                 throw "Length to long"; |  | ||||||
|             } |  | ||||||
|             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) |  | ||||||
|                 } |  | ||||||
|                 str += self.preferences.data[key] ?? ""; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             source.setData(str); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         length.addCallback(l => { |  | ||||||
|             updateData(Number(l)); |  | ||||||
|         }); |  | ||||||
|         this.preferences.addCallbackAndRun(_ => { |  | ||||||
|             updateData(Number(length.data)); |  | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         return source; |         function updateData(l: number) { | ||||||
|  |             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) | ||||||
|  |             if (prefsCount > 100) { | ||||||
|  |                 throw "Length to long" | ||||||
|  |             } | ||||||
|  |             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 | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |                 str += self.preferences.data[key] ?? "" | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             source.setData(str) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         length.addCallback((l) => { | ||||||
|  |             updateData(Number(l)) | ||||||
|  |         }) | ||||||
|  |         this.preferences.addCallbackAndRun((_) => { | ||||||
|  |             updateData(Number(length.data)) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         return source | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GetPreference(key: string, defaultValue : string = undefined, prefix: string = "mapcomplete-"): UIEventSource<string> { |     public GetPreference( | ||||||
|         if(key.startsWith(prefix) && prefix !== ""){ |         key: string, | ||||||
|             console.trace("A preference was requested which has a duplicate prefix in its key. This is probably a bug") |         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 = prefix + key | ||||||
|         key = key.replace(/[:\\\/"' {}.%]/g, '') |         key = key.replace(/[:\\\/"' {}.%]/g, "") | ||||||
|         if (key.length >= 255) { |         if (key.length >= 255) { | ||||||
|             throw "Preferences: key length to big"; |             throw "Preferences: key length to big" | ||||||
|         } |         } | ||||||
|         const cached = this.preferenceSources.get(key) |         const cached = this.preferenceSources.get(key) | ||||||
|         if (cached !== undefined) { |         if (cached !== undefined) { | ||||||
|             return cached; |             return cached | ||||||
|         } |         } | ||||||
|         if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) { |         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); |  | ||||||
|         pref.addCallback((v) => { |  | ||||||
|             this.UploadPreference(key, v); |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|          |         const pref = new UIEventSource<string>( | ||||||
|  |             this.preferences.data[key] ?? defaultValue, | ||||||
|  |             "osm-preference:" + key | ||||||
|  |         ) | ||||||
|  |         pref.addCallback((v) => { | ||||||
|  |             this.UploadPreference(key, v) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|         this.preferenceSources.set(key, pref) |         this.preferenceSources.set(key, pref) | ||||||
|         return pref; |         return pref | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public ClearPreferences() { |     public ClearPreferences() { | ||||||
|         let isRunning = false; |         let isRunning = false | ||||||
|         const self = this; |         const self = this | ||||||
|         this.preferences.addCallback(prefs => { |         this.preferences.addCallback((prefs) => { | ||||||
|             console.log("Cleaning preferences...") |             console.log("Cleaning preferences...") | ||||||
|             if (Object.keys(prefs).length == 0) { |             if (Object.keys(prefs).length == 0) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             if (isRunning) { |             if (isRunning) { | ||||||
|                 return |                 return | ||||||
|  | @ -149,94 +162,98 @@ export class OsmPreferences { | ||||||
|             isRunning = true |             isRunning = true | ||||||
|             const prefixes = ["mapcomplete-"] |             const prefixes = ["mapcomplete-"] | ||||||
|             for (const key in prefs) { |             for (const key in prefs) { | ||||||
|                 const matches = prefixes.some(prefix => key.startsWith(prefix)) |                 const matches = prefixes.some((prefix) => key.startsWith(prefix)) | ||||||
|                 if (matches) { |                 if (matches) { | ||||||
|                     console.log("Clearing ", key) |                     console.log("Clearing ", key) | ||||||
|                     self.GetPreference(key, "", "").setData("") |                     self.GetPreference(key, "", "").setData("") | ||||||
| 
 |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             isRunning = false; |             isRunning = false | ||||||
|             return; |             return | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private UpdatePreferences() { |     private UpdatePreferences() { | ||||||
|         const self = this; |         const self = this | ||||||
|         this.auth.xhr({ |         this.auth.xhr( | ||||||
|             method: 'GET', |             { | ||||||
|             path: '/api/0.6/user/preferences' |                 method: "GET", | ||||||
|         }, function (error, value: XMLDocument) { |                 path: "/api/0.6/user/preferences", | ||||||
|             if (error) { |             }, | ||||||
|                 console.log("Could not load preferences", error); |             function (error, value: XMLDocument) { | ||||||
|                 return; |                 if (error) { | ||||||
|             } |                     console.log("Could not load preferences", error) | ||||||
|             const prefs = value.getElementsByTagName("preference"); |                     return | ||||||
|             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; |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             // 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){ |  | ||||||
|                     // OSM doesn't know this value yet
 |  | ||||||
|                     self.UploadPreference(key, preference.data) |  | ||||||
|                 } else { |  | ||||||
|                     // OSM does have a value - set it
 |  | ||||||
|                     preference.setData(osmValue) |  | ||||||
|                 } |                 } | ||||||
|             }) |                 const prefs = value.getElementsByTagName("preference") | ||||||
|              |                 for (let i = 0; i < prefs.length; i++) { | ||||||
|             self.preferences.ping(); |                     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) { | ||||||
|  |                         // OSM doesn't know this value yet
 | ||||||
|  |                         self.UploadPreference(key, preference.data) | ||||||
|  |                     } else { | ||||||
|  |                         // OSM does have a value - set it
 | ||||||
|  |                         preference.setData(osmValue) | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  | 
 | ||||||
|  |                 self.preferences.ping() | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private UploadPreference(k: string, v: string) { |     private UploadPreference(k: string, v: string) { | ||||||
|         if (!this.userDetails.data.loggedIn) { |         if (!this.userDetails.data.loggedIn) { | ||||||
|             console.debug(`Not saving preference ${k}: user not logged in`); |             console.debug(`Not saving preference ${k}: user not logged in`) | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (this.preferences.data[k] === v) { |         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 === "") { |         if (v === undefined || v === "") { | ||||||
|             this.auth.xhr({ |             this.auth.xhr( | ||||||
|                 method: 'DELETE', |                 { | ||||||
|                 path: '/api/0.6/user/preferences/' + encodeURIComponent(k), |                     method: "DELETE", | ||||||
|                 options: {header: {'Content-Type': 'text/plain'}}, |                     path: "/api/0.6/user/preferences/" + encodeURIComponent(k), | ||||||
|             }, function (error) { |                     options: { header: { "Content-Type": "text/plain" } }, | ||||||
|                 if (error) { |                 }, | ||||||
|                     console.warn("Could not remove preference", error); |                 function (error) { | ||||||
|                     return; |                     if (error) { | ||||||
|  |                         console.warn("Could not remove preference", error) | ||||||
|  |                         return | ||||||
|  |                     } | ||||||
|  |                     console.debug("Preference ", k, "removed!") | ||||||
|                 } |                 } | ||||||
|                 console.debug("Preference ", k, "removed!"); |             ) | ||||||
| 
 |             return | ||||||
|             }); |  | ||||||
|             return; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |         this.auth.xhr( | ||||||
|         this.auth.xhr({ |             { | ||||||
|             method: 'PUT', |                 method: "PUT", | ||||||
|             path: '/api/0.6/user/preferences/' + encodeURIComponent(k), |                 path: "/api/0.6/user/preferences/" + encodeURIComponent(k), | ||||||
|             options: {header: {'Content-Type': 'text/plain'}}, |                 options: { header: { "Content-Type": "text/plain" } }, | ||||||
|             content: v |                 content: v, | ||||||
|         }, function (error) { |             }, | ||||||
|             if (error) { |             function (error) { | ||||||
|                 console.warn(`Could not set preference "${k}"'`, error); |                 if (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 { TagsFilter } from "../Tags/TagsFilter" | ||||||
| import RelationsTracker from "./RelationsTracker"; | import RelationsTracker from "./RelationsTracker" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import {ImmutableStore, Store} from "../UIEventSource"; | import { ImmutableStore, Store } from "../UIEventSource" | ||||||
| import {BBox} from "../BBox"; | import { BBox } from "../BBox" | ||||||
| import * as osmtogeojson from "osmtogeojson"; | import * as osmtogeojson from "osmtogeojson" | ||||||
| import {FeatureCollection} from "@turf/turf"; | import { FeatureCollection } from "@turf/turf" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Interfaces overpass to get all the latest data |  * Interfaces overpass to get all the latest data | ||||||
|  */ |  */ | ||||||
| export class Overpass { | export class Overpass { | ||||||
|     private _filter: TagsFilter |     private _filter: TagsFilter | ||||||
|     private readonly _interpreterUrl: string; |     private readonly _interpreterUrl: string | ||||||
|     private readonly _timeout: Store<number>; |     private readonly _timeout: Store<number> | ||||||
|     private readonly _extraScripts: string[]; |     private readonly _extraScripts: string[] | ||||||
|     private _includeMeta: boolean; |     private _includeMeta: boolean | ||||||
|     private _relationTracker: RelationsTracker; |     private _relationTracker: RelationsTracker | ||||||
| 
 | 
 | ||||||
|     constructor(filter: TagsFilter, |     constructor( | ||||||
|                 extraScripts: string[], |         filter: TagsFilter, | ||||||
|                 interpreterUrl: string, |         extraScripts: string[], | ||||||
|                 timeout?: Store<number>, |         interpreterUrl: string, | ||||||
|                 relationTracker?: RelationsTracker, |         timeout?: Store<number>, | ||||||
|                 includeMeta = true) { |         relationTracker?: RelationsTracker, | ||||||
|         this._timeout = timeout ?? new ImmutableStore<number>(90); |         includeMeta = true | ||||||
|         this._interpreterUrl = interpreterUrl; |     ) { | ||||||
|  |         this._timeout = timeout ?? new ImmutableStore<number>(90) | ||||||
|  |         this._interpreterUrl = interpreterUrl | ||||||
|         const optimized = filter.optimize() |         const optimized = filter.optimize() | ||||||
|         if(optimized === true || optimized === false){ |         if (optimized === true || optimized === false) { | ||||||
|             throw "Invalid filter: optimizes to true of false" |             throw "Invalid filter: optimizes to true of false" | ||||||
|         } |         } | ||||||
|         this._filter = optimized |         this._filter = optimized | ||||||
|         this._extraScripts = extraScripts; |         this._extraScripts = extraScripts | ||||||
|         this._includeMeta = includeMeta; |         this._includeMeta = includeMeta | ||||||
|         this._relationTracker = relationTracker |         this._relationTracker = relationTracker | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async queryGeoJson(bounds: BBox): Promise<[FeatureCollection, Date]> { |     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) |         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)}` |         return `${this._interpreterUrl}?data=${encodeURIComponent(query)}` | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     public async ExecuteQuery(query: string):Promise<[FeatureCollection, Date]>  { |     public async ExecuteQuery(query: string): Promise<[FeatureCollection, Date]> { | ||||||
|         const self = this; |         const self = this | ||||||
|         const json = await Utils.downloadJson(this.buildUrl(query)) |         const json = await Utils.downloadJson(this.buildUrl(query)) | ||||||
| 
 | 
 | ||||||
|         if (json.elements.length === 0 && json.remark !== undefined) { |         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}` |             throw `Runtime error (timeout or similar)${json.remark}` | ||||||
|         } |         } | ||||||
|         if (json.elements.length === 0) { |         if (json.elements.length === 0) { | ||||||
|  | @ -58,77 +69,81 @@ export class Overpass { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         self._relationTracker?.RegisterRelations(json) |         self._relationTracker?.RegisterRelations(json) | ||||||
|         const geojson = osmtogeojson.default(json); |         const geojson = osmtogeojson.default(json) | ||||||
|         const osmTime = new Date(json.osm3s.timestamp_osm_base); |         const osmTime = new Date(json.osm3s.timestamp_osm_base) | ||||||
|         return [<any> geojson, osmTime]; |         return [<any>geojson, osmTime] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Constructs the actual script to execute on Overpass |      * Constructs the actual script to execute on Overpass | ||||||
|      * 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink' |      * 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink' | ||||||
|      *  |      * | ||||||
|      * import {Tag} from "../Tags/Tag"; |      * import {Tag} from "../Tags/Tag"; | ||||||
|      *  |      * | ||||||
|      * new Overpass(new Tag("key","value"), [], "").buildScript("{{bbox}}") // => `[out:json][timeout:90]{{bbox}};(nwr["key"="value"];);out body;out meta;>;out skel qt;`
 |      * new Overpass(new Tag("key","value"), [], "").buildScript("{{bbox}}") // => `[out:json][timeout:90]{{bbox}};(nwr["key"="value"];);out body;out meta;>;out skel qt;`
 | ||||||
|      */ |      */ | ||||||
|     public buildScript(bbox: string, postCall: string = "", pretty = false): string { |     public buildScript(bbox: string, postCall: string = "", pretty = false): string { | ||||||
|         const filters = this._filter.asOverpass() |         const filters = this._filter.asOverpass() | ||||||
|         let filter = "" |         let filter = "" | ||||||
|         for (const filterOr of filters) { |         for (const filterOr of filters) { | ||||||
|             if(pretty){ |             if (pretty) { | ||||||
|                 filter += "    " |                 filter += "    " | ||||||
|             } |             } | ||||||
|             filter += 'nwr' + filterOr + postCall + ';' |             filter += "nwr" + filterOr + postCall + ";" | ||||||
|             if(pretty){ |             if (pretty) { | ||||||
|                 filter+="\n" |                 filter += "\n" | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         for (const extraScript of this._extraScripts) { |         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 |      * Constructs the actual script to execute on Overpass with geocoding | ||||||
|      * 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink' |      * '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() |         const filters = this._filter.asOverpass() | ||||||
|         let filter = "" |         let filter = "" | ||||||
|         for (const filterOr of filters) { |         for (const filterOr of filters) { | ||||||
|             if(pretty){ |             if (pretty) { | ||||||
|                 filter += "    " |                 filter += "    " | ||||||
|             } |             } | ||||||
|             filter += 'nwr' + filterOr + '(area.searchArea);' |             filter += "nwr" + filterOr + "(area.searchArea);" | ||||||
|             if(pretty){ |             if (pretty) { | ||||||
|                 filter+="\n" |                 filter += "\n" | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         for (const extraScript of this._extraScripts) { |         for (const extraScript of this._extraScripts) { | ||||||
|             filter += '(' + extraScript + ');'; |             filter += "(" + extraScript + ");" | ||||||
|         } |         } | ||||||
|         let id = area.osm_id; |         let id = area.osm_id | ||||||
|         if(area.osm_type === "relation"){ |         if (area.osm_type === "relation") { | ||||||
|             id += 3600000000 |             id += 3600000000 | ||||||
|         } |         } | ||||||
|         return`[out:json][timeout:${this._timeout.data}];
 |         return `[out:json][timeout:${this._timeout.data}];
 | ||||||
|         area(id:${id})->.searchArea; |         area(id:${id})->.searchArea; | ||||||
|         (${filter}); |         (${filter}); | ||||||
|         out body;${this._includeMeta ? 'out meta;' : ''}>;out skel qt;` |         out body;${this._includeMeta ? "out meta;" : ""}>;out skel qt;` | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|      |  | ||||||
|     public buildQuery(bbox: string) { |     public buildQuery(bbox: string) { | ||||||
|         return this.buildUrl(this.buildScript(bbox)) |         return this.buildUrl(this.buildScript(bbox)) | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     /** |     /** | ||||||
|      * Little helper method to quickly open overpass-turbo in the browser |      * 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 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=" |         const url = "http://overpass-turbo.eu/?Q=" | ||||||
|         return url + encodeURIComponent(script) |         return url + encodeURIComponent(script) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,24 +1,25 @@ | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| 
 | 
 | ||||||
| export interface Relation { | export interface Relation { | ||||||
|     id: number, |     id: number | ||||||
|     type: "relation" |     type: "relation" | ||||||
|     members: { |     members: { | ||||||
|         type: ("way" | "node" | "relation"), |         type: "way" | "node" | "relation" | ||||||
|         ref: number, |         ref: number | ||||||
|         role: string |         role: string | ||||||
|     }[], |     }[] | ||||||
|     tags: any, |     tags: any | ||||||
|     // Alias for tags; tags == properties
 |     // Alias for tags; tags == properties
 | ||||||
|     properties: any |     properties: any | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default class RelationsTracker { | 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 |      * Gets an overview of the relations - except for multipolygons. We don't care about those | ||||||
|  | @ -26,8 +27,9 @@ export default class RelationsTracker { | ||||||
|      * @constructor |      * @constructor | ||||||
|      */ |      */ | ||||||
|     private static GetRelationElements(overpassJson: any): Relation[] { |     private static GetRelationElements(overpassJson: any): Relation[] { | ||||||
|         const relations = overpassJson.elements |         const relations = overpassJson.elements.filter( | ||||||
|             .filter(element => element.type === "relation" && element.tags.type !== "multipolygon") |             (element) => element.type === "relation" && element.tags.type !== "multipolygon" | ||||||
|  |         ) | ||||||
|         for (const relation of relations) { |         for (const relation of relations) { | ||||||
|             relation.properties = relation.tags |             relation.properties = relation.tags | ||||||
|         } |         } | ||||||
|  | @ -45,12 +47,12 @@ export default class RelationsTracker { | ||||||
|      */ |      */ | ||||||
|     private UpdateMembershipTable(relations: Relation[]): void { |     private UpdateMembershipTable(relations: Relation[]): void { | ||||||
|         const memberships = this.knownRelations.data |         const memberships = this.knownRelations.data | ||||||
|         let changed = false; |         let changed = false | ||||||
|         for (const relation of relations) { |         for (const relation of relations) { | ||||||
|             for (const member of relation.members) { |             for (const member of relation.members) { | ||||||
|                 const role = { |                 const role = { | ||||||
|                     role: member.role, |                     role: member.role, | ||||||
|                     relation: relation |                     relation: relation, | ||||||
|                 } |                 } | ||||||
|                 const key = member.type + "/" + member.ref |                 const key = member.type + "/" + member.ref | ||||||
|                 if (!memberships.has(key)) { |                 if (!memberships.has(key)) { | ||||||
|  | @ -58,19 +60,17 @@ export default class RelationsTracker { | ||||||
|                 } |                 } | ||||||
|                 const knownRelations = memberships.get(key) |                 const knownRelations = memberships.get(key) | ||||||
| 
 | 
 | ||||||
|                 const alreadyExists = knownRelations.some(knownRole => { |                 const alreadyExists = knownRelations.some((knownRole) => { | ||||||
|                     return knownRole.role === role.role && knownRole.relation === role.relation |                     return knownRole.role === role.role && knownRole.relation === role.relation | ||||||
|                 }) |                 }) | ||||||
|                 if (!alreadyExists) { |                 if (!alreadyExists) { | ||||||
|                     knownRelations.push(role) |                     knownRelations.push(role) | ||||||
|                     changed = true; |                     changed = true | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if (changed) { |         if (changed) { | ||||||
|             this.knownRelations.ping() |             this.knownRelations.ping() | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,13 +1,12 @@ | ||||||
| export default class AspectedRouting { | export default class AspectedRouting { | ||||||
| 
 |  | ||||||
|     public readonly name: string |     public readonly name: string | ||||||
|     public readonly description: string |     public readonly description: string | ||||||
|     public readonly units: string |     public readonly units: string | ||||||
|     public readonly program: any |     public readonly program: any | ||||||
| 
 | 
 | ||||||
|     public constructor(program) { |     public constructor(program) { | ||||||
|         this.name = program.name; |         this.name = program.name | ||||||
|         this.description = program.description; |         this.description = program.description | ||||||
|         this.units = program.unit |         this.units = program.unit | ||||||
|         this.program = JSON.parse(JSON.stringify(program)) |         this.program = JSON.parse(JSON.stringify(program)) | ||||||
|         delete this.program.name |         delete this.program.name | ||||||
|  | @ -20,40 +19,41 @@ export default class AspectedRouting { | ||||||
|      */ |      */ | ||||||
|     public static interpret(program: any, properties: any) { |     public static interpret(program: any, properties: any) { | ||||||
|         if (typeof program !== "object") { |         if (typeof program !== "object") { | ||||||
|             return program; |             return program | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let functionName /*: string*/ = undefined; |         let functionName /*: string*/ = undefined | ||||||
|         let functionArguments /*: any */ = undefined |         let functionArguments /*: any */ = undefined | ||||||
|         let otherValues = {} |         let otherValues = {} | ||||||
|         // @ts-ignore
 |         // @ts-ignore
 | ||||||
|         Object.entries(program).forEach(tag => { |         Object.entries(program).forEach((tag) => { | ||||||
|                 const [key, value] = tag; |             const [key, value] = tag | ||||||
|                 if (key.startsWith("$")) { |             if (key.startsWith("$")) { | ||||||
|                     functionName = key |                 functionName = key | ||||||
|                     functionArguments = value |                 functionArguments = value | ||||||
|                 } else { |             } else { | ||||||
|                     otherValues[key] = value |                 otherValues[key] = value | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         ) |         }) | ||||||
| 
 | 
 | ||||||
|         if (functionName === undefined) { |         if (functionName === undefined) { | ||||||
|             return AspectedRouting.interpretAsDictionary(program, properties) |             return AspectedRouting.interpretAsDictionary(program, properties) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (functionName === '$multiply') { |         if (functionName === "$multiply") { | ||||||
|             return AspectedRouting.multiplyScore(properties, functionArguments); |             return AspectedRouting.multiplyScore(properties, functionArguments) | ||||||
|         } else if (functionName === '$firstMatchOf') { |         } else if (functionName === "$firstMatchOf") { | ||||||
|             return AspectedRouting.getFirstMatchScore(properties, functionArguments); |             return AspectedRouting.getFirstMatchScore(properties, functionArguments) | ||||||
|         } else if (functionName === '$min') { |         } else if (functionName === "$min") { | ||||||
|             return AspectedRouting.getMinValue(properties, functionArguments); |             return AspectedRouting.getMinValue(properties, functionArguments) | ||||||
|         } else if (functionName === '$max') { |         } else if (functionName === "$max") { | ||||||
|             return AspectedRouting.getMaxValue(properties, functionArguments); |             return AspectedRouting.getMaxValue(properties, functionArguments) | ||||||
|         } else if (functionName === '$default') { |         } else if (functionName === "$default") { | ||||||
|             return AspectedRouting.defaultV(functionArguments, otherValues, properties) |             return AspectedRouting.defaultV(functionArguments, otherValues, properties) | ||||||
|         } else { |         } 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)}` | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -70,7 +70,7 @@ export default class AspectedRouting { | ||||||
|      *     surface: { |      *     surface: { | ||||||
|      *         sett : 0.9 |      *         sett : 0.9 | ||||||
|      *     } |      *     } | ||||||
|      *      |      * | ||||||
|      * } |      * } | ||||||
|      * |      * | ||||||
|      * in combination with the tags {highway: residential}, |      * in combination with the tags {highway: residential}, | ||||||
|  | @ -86,8 +86,8 @@ export default class AspectedRouting { | ||||||
|      */ |      */ | ||||||
|     private static interpretAsDictionary(program, tags) { |     private static interpretAsDictionary(program, tags) { | ||||||
|         // @ts-ignore
 |         // @ts-ignore
 | ||||||
|         return Object.entries(tags).map(tag => { |         return Object.entries(tags).map((tag) => { | ||||||
|             const [key, value] = tag; |             const [key, value] = tag | ||||||
|             const propertyValue = program[key] |             const propertyValue = program[key] | ||||||
|             if (propertyValue === undefined) { |             if (propertyValue === undefined) { | ||||||
|                 return undefined |                 return undefined | ||||||
|  | @ -97,7 +97,7 @@ export default class AspectedRouting { | ||||||
|             } |             } | ||||||
|             // @ts-ignore
 |             // @ts-ignore
 | ||||||
|             return propertyValue[value] |             return propertyValue[value] | ||||||
|         }); |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static defaultV(subProgram, otherArgs, tags) { |     private static defaultV(subProgram, otherArgs, tags) { | ||||||
|  | @ -105,7 +105,7 @@ export default class AspectedRouting { | ||||||
|         const normalProgram = Object.entries(otherArgs)[0][1] |         const normalProgram = Object.entries(otherArgs)[0][1] | ||||||
|         const value = AspectedRouting.interpret(normalProgram, tags) |         const value = AspectedRouting.interpret(normalProgram, tags) | ||||||
|         if (value !== undefined) { |         if (value !== undefined) { | ||||||
|             return value; |             return value | ||||||
|         } |         } | ||||||
|         return AspectedRouting.interpret(subProgram, tags) |         return AspectedRouting.interpret(subProgram, tags) | ||||||
|     } |     } | ||||||
|  | @ -121,13 +121,15 @@ export default class AspectedRouting { | ||||||
| 
 | 
 | ||||||
|         let subResults: any[] |         let subResults: any[] | ||||||
|         if (subprograms.length !== undefined) { |         if (subprograms.length !== undefined) { | ||||||
|             subResults = AspectedRouting.concatMap(subprograms, subprogram => AspectedRouting.interpret(subprogram, tags)) |             subResults = AspectedRouting.concatMap(subprograms, (subprogram) => | ||||||
|  |                 AspectedRouting.interpret(subprogram, tags) | ||||||
|  |             ) | ||||||
|         } else { |         } else { | ||||||
|             subResults = AspectedRouting.interpret(subprograms, tags) |             subResults = AspectedRouting.interpret(subprograms, tags) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         subResults.filter(r => r !== undefined).forEach(r => number *= parseFloat(r)) |         subResults.filter((r) => r !== undefined).forEach((r) => (number *= parseFloat(r))) | ||||||
|         return number.toFixed(2); |         return number.toFixed(2) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static getFirstMatchScore(tags, order: any) { |     private static getFirstMatchScore(tags, order: any) { | ||||||
|  | @ -136,12 +138,12 @@ export default class AspectedRouting { | ||||||
|         for (let key of order) { |         for (let key of order) { | ||||||
|             // @ts-ignore
 |             // @ts-ignore
 | ||||||
|             for (let entry of Object.entries(JSON.parse(tags))) { |             for (let entry of Object.entries(JSON.parse(tags))) { | ||||||
|                 const [tagKey, value] = entry; |                 const [tagKey, value] = entry | ||||||
|                 if (key === tagKey) { |                 if (key === tagKey) { | ||||||
|                     // We have a match... let's evaluate the subprogram
 |                     // We have a match... let's evaluate the subprogram
 | ||||||
|                     const evaluated = AspectedRouting.interpret(value, tags) |                     const evaluated = AspectedRouting.interpret(value, tags) | ||||||
|                     if (evaluated !== undefined) { |                     if (evaluated !== undefined) { | ||||||
|                         return evaluated; |                         return evaluated | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  | @ -152,26 +154,30 @@ export default class AspectedRouting { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static getMinValue(tags, subprogram) { |     private static getMinValue(tags, subprogram) { | ||||||
|         const minArr = subprogram.map(part => { |         const minArr = subprogram | ||||||
|             if (typeof (part) === 'object') { |             .map((part) => { | ||||||
|                 const calculatedValue = this.interpret(part, tags) |                 if (typeof part === "object") { | ||||||
|                 return parseFloat(calculatedValue) |                     const calculatedValue = this.interpret(part, tags) | ||||||
|             } else { |                     return parseFloat(calculatedValue) | ||||||
|                 return parseFloat(part); |                 } else { | ||||||
|             } |                     return parseFloat(part) | ||||||
|         }).filter(v => !isNaN(v)); |                 } | ||||||
|         return Math.min(...minArr); |             }) | ||||||
|  |             .filter((v) => !isNaN(v)) | ||||||
|  |         return Math.min(...minArr) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static getMaxValue(tags, subprogram) { |     private static getMaxValue(tags, subprogram) { | ||||||
|         const maxArr = subprogram.map(part => { |         const maxArr = subprogram | ||||||
|             if (typeof (part) === 'object') { |             .map((part) => { | ||||||
|                 return parseFloat(AspectedRouting.interpret(part, tags)) |                 if (typeof part === "object") { | ||||||
|             } else { |                     return parseFloat(AspectedRouting.interpret(part, tags)) | ||||||
|                 return parseFloat(part); |                 } else { | ||||||
|             } |                     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[] { |     private static concatMap(list, f): any[] { | ||||||
|  | @ -185,11 +191,10 @@ export default class AspectedRouting { | ||||||
|                 result.push(elem) |                 result.push(elem) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return result; |         return result | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public evaluate(properties) { |     public evaluate(properties) { | ||||||
|         return AspectedRouting.interpret(this.program, properties) |         return AspectedRouting.interpret(this.program, properties) | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,107 +1,125 @@ | ||||||
| import {GeoOperations} from "./GeoOperations"; | import { GeoOperations } from "./GeoOperations" | ||||||
| import {Utils} from "../Utils"; | import { Utils } from "../Utils" | ||||||
| import opening_hours from "opening_hours"; | import opening_hours from "opening_hours" | ||||||
| import Combine from "../UI/Base/Combine"; | import Combine from "../UI/Base/Combine" | ||||||
| import BaseUIElement from "../UI/BaseUIElement"; | import BaseUIElement from "../UI/BaseUIElement" | ||||||
| import Title from "../UI/Base/Title"; | import Title from "../UI/Base/Title" | ||||||
| import {FixedUiElement} from "../UI/Base/FixedUiElement"; | import { FixedUiElement } from "../UI/Base/FixedUiElement" | ||||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||||
| import {CountryCoder} from "latlon2country" | import { CountryCoder } from "latlon2country" | ||||||
| import Constants from "../Models/Constants"; | import Constants from "../Models/Constants" | ||||||
| import {TagUtils} from "./Tags/TagUtils"; | import { TagUtils } from "./Tags/TagUtils" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| export class SimpleMetaTagger { | export class SimpleMetaTagger { | ||||||
|     public readonly keys: string[]; |     public readonly keys: string[] | ||||||
|     public readonly doc: string; |     public readonly doc: string | ||||||
|     public readonly isLazy: boolean; |     public readonly isLazy: boolean | ||||||
|     public readonly includesDates: 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 |      * A function that adds some extra data to a feature | ||||||
|      * @param docs: what does this extra data do? |      * @param docs: what does this extra data do? | ||||||
|      * @param f: apply the changes. Returns true if something changed |      * @param f: apply the changes. Returns true if something changed | ||||||
|      */ |      */ | ||||||
|     constructor(docs: { keys: string[], doc: string, includesDates?: boolean, isLazy?: boolean, cleanupRetagger?: boolean }, |     constructor( | ||||||
|                 f: ((feature: any, freshness: Date, layer: LayerConfig, state) => boolean)) { |         docs: { | ||||||
|         this.keys = docs.keys; |             keys: string[] | ||||||
|         this.doc = docs.doc; |             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.isLazy = docs.isLazy | ||||||
|         this.applyMetaTagsOnFeature = f; |         this.applyMetaTagsOnFeature = f | ||||||
|         this.includesDates = docs.includesDates ?? false; |         this.includesDates = docs.includesDates ?? false | ||||||
|         if (!docs.cleanupRetagger) { |         if (!docs.cleanupRetagger) { | ||||||
|             for (const key of docs.keys) { |             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 (_)` |                     throw `Incorrect key for a calculated meta value '${key}': it should start with underscore (_)` | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class CountryTagger extends SimpleMetaTagger { | export class CountryTagger extends SimpleMetaTagger { | ||||||
|     private static readonly coder = new CountryCoder(Constants.countryCoderEndpoint, Utils.downloadJson); |     private static readonly coder = new CountryCoder( | ||||||
|     public runningTasks: Set<any>; |         Constants.countryCoderEndpoint, | ||||||
|  |         Utils.downloadJson | ||||||
|  |     ) | ||||||
|  |     public runningTasks: Set<any> | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         const runningTasks = new Set<any>(); |         const runningTasks = new Set<any>() | ||||||
|         super |         super( | ||||||
|         ( |  | ||||||
|             { |             { | ||||||
|                 keys: ["_country"], |                 keys: ["_country"], | ||||||
|                 doc: "The country code of the property (with latlon2country)", |                 doc: "The country code of the property (with latlon2country)", | ||||||
|                 includesDates: false |                 includesDates: false, | ||||||
|             }, |             }, | ||||||
|             ((feature, _, __, state) => { |             (feature, _, __, state) => { | ||||||
|                 let centerPoint: any = GeoOperations.centerpoint(feature); |                 let centerPoint: any = GeoOperations.centerpoint(feature) | ||||||
|                 const lat = centerPoint.geometry.coordinates[1]; |                 const lat = centerPoint.geometry.coordinates[1] | ||||||
|                 const lon = centerPoint.geometry.coordinates[0]; |                 const lon = centerPoint.geometry.coordinates[0] | ||||||
|                 runningTasks.add(feature) |                 runningTasks.add(feature) | ||||||
|                 CountryTagger.coder.GetCountryCodeAsync(lon, lat).then( |                 CountryTagger.coder | ||||||
|                     countries => { |                     .GetCountryCodeAsync(lon, lat) | ||||||
|  |                     .then((countries) => { | ||||||
|                         runningTasks.delete(feature) |                         runningTasks.delete(feature) | ||||||
|                         try { |                         try { | ||||||
|                             const oldCountry = feature.properties["_country"]; |                             const oldCountry = feature.properties["_country"] | ||||||
|                             feature.properties["_country"] = countries[0].trim().toLowerCase(); |                             feature.properties["_country"] = countries[0].trim().toLowerCase() | ||||||
|                             if (oldCountry !== feature.properties["_country"]) { |                             if (oldCountry !== feature.properties["_country"]) { | ||||||
|                                 const tagsSource = state?.allElements?.getEventSourceById(feature.properties.id); |                                 const tagsSource = state?.allElements?.getEventSourceById( | ||||||
|                                 tagsSource?.ping(); |                                     feature.properties.id | ||||||
|  |                                 ) | ||||||
|  |                                 tagsSource?.ping() | ||||||
|                             } |                             } | ||||||
|                         } catch (e) { |                         } catch (e) { | ||||||
|                             console.warn(e) |                             console.warn(e) | ||||||
|                         } |                         } | ||||||
|                     } |                     }) | ||||||
|                 ).catch(_ => { |                     .catch((_) => { | ||||||
|                     runningTasks.delete(feature) |                         runningTasks.delete(feature) | ||||||
|                 }) |                     }) | ||||||
|                 return false; |                 return false | ||||||
|             }) |             } | ||||||
|         ) |         ) | ||||||
|         this.runningTasks = runningTasks; |         this.runningTasks = runningTasks | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default class SimpleMetaTaggers { | export default class SimpleMetaTaggers { | ||||||
| 
 |  | ||||||
|     public static readonly objectMetaInfo = new SimpleMetaTagger( |     public static readonly objectMetaInfo = new SimpleMetaTagger( | ||||||
|         { |         { | ||||||
|             keys: ["_last_edit:contributor", |             keys: [ | ||||||
|  |                 "_last_edit:contributor", | ||||||
|                 "_last_edit:contributor:uid", |                 "_last_edit:contributor:uid", | ||||||
|                 "_last_edit:changeset", |                 "_last_edit:changeset", | ||||||
|                 "_last_edit:timestamp", |                 "_last_edit:timestamp", | ||||||
|                 "_version_number", |                 "_version_number", | ||||||
|                 "_backend"], |                 "_backend", | ||||||
|             doc: "Information about the last edit of this object." |             ], | ||||||
|  |             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) { |             function move(src: string, target: string) { | ||||||
|                 if (tgs[src] === undefined) { |                 if (tgs[src] === undefined) { | ||||||
|                     return; |                     return | ||||||
|                 } |                 } | ||||||
|                 tgs[target] = tgs[src] |                 tgs[target] = tgs[src] | ||||||
|                 delete tgs[src] |                 delete tgs[src] | ||||||
|  | @ -112,7 +130,7 @@ export default class SimpleMetaTaggers { | ||||||
|             move("changeset", "_last_edit:changeset") |             move("changeset", "_last_edit:changeset") | ||||||
|             move("timestamp", "_last_edit:timestamp") |             move("timestamp", "_last_edit:timestamp") | ||||||
|             move("version", "_version_number") |             move("version", "_version_number") | ||||||
|             return true; |             return true | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     public static country = new CountryTagger() |     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`", |             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, _) => { |         (feature, _) => { | ||||||
|             const changed = feature.properties["_geometry:type"] === feature.geometry.type; |             const changed = feature.properties["_geometry:type"] === feature.geometry.type | ||||||
|             feature.properties["_geometry:type"] = feature.geometry.type; |             feature.properties["_geometry:type"] = feature.geometry.type | ||||||
|             return changed |             return changed | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     private static readonly cardinalDirections = { |     private static readonly cardinalDirections = { | ||||||
|         N: 0, NNE: 22.5, NE: 45, ENE: 67.5, |         N: 0, | ||||||
|         E: 90, ESE: 112.5, SE: 135, SSE: 157.5, |         NNE: 22.5, | ||||||
|         S: 180, SSW: 202.5, SW: 225, WSW: 247.5, |         NE: 45, | ||||||
|         W: 270, WNW: 292.5, NW: 315, NNW: 337.5 |         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"], |             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 => { |         (feature) => { | ||||||
|             const centerPoint = GeoOperations.centerpoint(feature); |             const centerPoint = GeoOperations.centerpoint(feature) | ||||||
|             const lat = centerPoint.geometry.coordinates[1]; |             const lat = centerPoint.geometry.coordinates[1] | ||||||
|             const lon = centerPoint.geometry.coordinates[0]; |             const lon = centerPoint.geometry.coordinates[0] | ||||||
|             feature.properties["_lat"] = "" + lat; |             feature.properties["_lat"] = "" + lat | ||||||
|             feature.properties["_lon"] = "" + lon; |             feature.properties["_lon"] = "" + lon | ||||||
|             feature._lon = lon; // This is dirty, I know
 |             feature._lon = lon // This is dirty, I know
 | ||||||
|             feature._lat = lat; |             feature._lat = lat | ||||||
|             return true; |             return true | ||||||
|         }) |         } | ||||||
|     ); |     ) | ||||||
|     private static layerInfo = new SimpleMetaTagger( |     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.", |             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) => { |         (feature, freshness, layer) => { | ||||||
|             if (feature.properties._layer === layer.id) { |             if (feature.properties._layer === layer.id) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|             feature.properties._layer = layer.id |             feature.properties._layer = layer.id | ||||||
|             return true; |             return true | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     private static noBothButLeftRight = new SimpleMetaTagger( |     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", |             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, |             includesDates: false, | ||||||
|             cleanupRetagger: true |             cleanupRetagger: true, | ||||||
|         }, |         }, | ||||||
|         ((feature, state, layer) => { |         (feature, state, layer) => { | ||||||
| 
 |             if (!layer.lineRendering.some((lr) => lr.leftRightSensitive)) { | ||||||
|             if (!layer.lineRendering.some(lr => lr.leftRightSensitive)) { |                 return | ||||||
|                 return; |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return SimpleMetaTaggers.removeBothTagging(feature.properties) |             return SimpleMetaTaggers.removeBothTagging(feature.properties) | ||||||
|         }) |         } | ||||||
|     ) |     ) | ||||||
|     private static surfaceArea = new SimpleMetaTagger( |     private static surfaceArea = new SimpleMetaTagger( | ||||||
|         { |         { | ||||||
|             keys: ["_surface", "_surface:ha"], |             keys: ["_surface", "_surface:ha"], | ||||||
|             doc: "The surface area of the feature, in square meters and in hectare. Not set on points and ways", |             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", { |             Object.defineProperty(feature.properties, "_surface", { | ||||||
|                 enumerable: false, |                 enumerable: false, | ||||||
|                 configurable: true, |                 configurable: true, | ||||||
|                 get: () => { |                 get: () => { | ||||||
|                     const sqMeters = "" + GeoOperations.surfaceAreaInSqMeters(feature); |                     const sqMeters = "" + GeoOperations.surfaceAreaInSqMeters(feature) | ||||||
|                     delete feature.properties["_surface"] |                     delete feature.properties["_surface"] | ||||||
|                     feature.properties["_surface"] = sqMeters; |                     feature.properties["_surface"] = sqMeters | ||||||
|                     return sqMeters |                     return sqMeters | ||||||
|                 } |                 }, | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
|             Object.defineProperty(feature.properties, "_surface:ha", { |             Object.defineProperty(feature.properties, "_surface:ha", { | ||||||
|                 enumerable: false, |                 enumerable: false, | ||||||
|                 configurable: true, |                 configurable: true, | ||||||
|                 get: () => { |                 get: () => { | ||||||
|                     const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature); |                     const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature) | ||||||
|                     const sqMetersHa = "" + Math.floor(sqMeters / 1000) / 10; |                     const sqMetersHa = "" + Math.floor(sqMeters / 1000) / 10 | ||||||
|                     delete feature.properties["_surface:ha"] |                     delete feature.properties["_surface:ha"] | ||||||
|                     feature.properties["_surface:ha"] = sqMetersHa; |                     feature.properties["_surface:ha"] = sqMetersHa | ||||||
|                     return sqMetersHa |                     return sqMetersHa | ||||||
|                 } |                 }, | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
|             return true; |             return true | ||||||
|         }) |         } | ||||||
|     ); |     ) | ||||||
|     private static levels = new SimpleMetaTagger( |     private static levels = new SimpleMetaTagger( | ||||||
|         { |         { | ||||||
|             doc: "Extract the 'level'-tag into a normalized, ';'-separated value", |             doc: "Extract the 'level'-tag into a normalized, ';'-separated value", | ||||||
|             keys: ["_level"] |             keys: ["_level"], | ||||||
|         }, |         }, | ||||||
|         ((feature) => { |         (feature) => { | ||||||
|             if (feature.properties["level"] === undefined) { |             if (feature.properties["level"] === undefined) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|              | 
 | ||||||
|             const l = feature.properties["level"] |             const l = feature.properties["level"] | ||||||
|             const newValue = TagUtils.LevelsParser(l).join(";") |             const newValue = TagUtils.LevelsParser(l).join(";") | ||||||
|             if(l === newValue) { |             if (l === newValue) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|             feature.properties["level"] = newValue |             feature.properties["level"] = newValue | ||||||
|             return true |             return true | ||||||
| 
 |         } | ||||||
|         }) |  | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     private static canonicalize = new SimpleMetaTagger( |     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)", |             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"], |             keys: ["Theme-defined keys"], | ||||||
| 
 |  | ||||||
|         }, |         }, | ||||||
|         ((feature, _, __, state) => { |         (feature, _, __, state) => { | ||||||
|             const units = Utils.NoNull([].concat(...state?.layoutToUse?.layers?.map(layer => layer.units) ?? [])); |             const units = Utils.NoNull( | ||||||
|  |                 [].concat(...(state?.layoutToUse?.layers?.map((layer) => layer.units) ?? [])) | ||||||
|  |             ) | ||||||
|             if (units.length == 0) { |             if (units.length == 0) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             let rewritten = false; |             let rewritten = false | ||||||
|             for (const key in feature.properties) { |             for (const key in feature.properties) { | ||||||
|                 if (!feature.properties.hasOwnProperty(key)) { |                 if (!feature.properties.hasOwnProperty(key)) { | ||||||
|                     continue; |                     continue | ||||||
|                 } |                 } | ||||||
|                 for (const unit of units) { |                 for (const unit of units) { | ||||||
|                     if (unit === undefined) { |                     if (unit === undefined) { | ||||||
|  | @ -258,56 +292,59 @@ export default class SimpleMetaTaggers { | ||||||
|                         continue |                         continue | ||||||
|                     } |                     } | ||||||
|                     if (!unit.appliesToKeys.has(key)) { |                     if (!unit.appliesToKeys.has(key)) { | ||||||
|                         continue; |                         continue | ||||||
|                     } |                     } | ||||||
|                     const value = feature.properties[key] |                     const value = feature.properties[key] | ||||||
|                     const denom = unit.findDenomination(value, () => feature.properties["_country"]) |                     const denom = unit.findDenomination(value, () => feature.properties["_country"]) | ||||||
|                     if (denom === undefined) { |                     if (denom === undefined) { | ||||||
|                         // no valid value found
 |                         // no valid value found
 | ||||||
|                         break; |                         break | ||||||
|                     } |                     } | ||||||
|                     const [, denomination] = denom; |                     const [, denomination] = denom | ||||||
|                     const defaultDenom = unit.getDefaultDenomination(() => feature.properties["_country"]) |                     const defaultDenom = unit.getDefaultDenomination( | ||||||
|                     let canonical = denomination?.canonicalValue(value, defaultDenom == denomination) ?? undefined; |                         () => feature.properties["_country"] | ||||||
|  |                     ) | ||||||
|  |                     let canonical = | ||||||
|  |                         denomination?.canonicalValue(value, defaultDenom == denomination) ?? | ||||||
|  |                         undefined | ||||||
|                     if (canonical === value) { |                     if (canonical === value) { | ||||||
|                         break; |                         break | ||||||
|                     } |                     } | ||||||
|                     console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`) |                     console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`) | ||||||
|                     if (canonical === undefined && !unit.eraseInvalid) { |                     if (canonical === undefined && !unit.eraseInvalid) { | ||||||
|                         break; |                         break | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     feature.properties[key] = canonical; |                     feature.properties[key] = canonical | ||||||
|                     rewritten = true; |                     rewritten = true | ||||||
|                     break; |                     break | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|             return rewritten |             return rewritten | ||||||
|         }) |         } | ||||||
|     ) |     ) | ||||||
|     private static lngth = new SimpleMetaTagger( |     private static lngth = new SimpleMetaTagger( | ||||||
|         { |         { | ||||||
|             keys: ["_length", "_length:km"], |             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) |             const l = GeoOperations.lengthInMeters(feature) | ||||||
|             feature.properties["_length"] = "" + l |             feature.properties["_length"] = "" + l | ||||||
|             const km = Math.floor(l / 1000) |             const km = Math.floor(l / 1000) | ||||||
|             const kmRest = Math.round((l - km * 1000) / 100) |             const kmRest = Math.round((l - km * 1000) / 100) | ||||||
|             feature.properties["_length:km"] = "" + km + "." + kmRest |             feature.properties["_length:km"] = "" + km + "." + kmRest | ||||||
|             return true; |             return true | ||||||
|         }) |         } | ||||||
|     ) |     ) | ||||||
|     private static isOpen = new SimpleMetaTagger( |     private static isOpen = new SimpleMetaTagger( | ||||||
|         { |         { | ||||||
|             keys: ["_isOpen"], |             keys: ["_isOpen"], | ||||||
|             doc: "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')", |             doc: "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')", | ||||||
|             includesDates: true, |             includesDates: true, | ||||||
|             isLazy: true |             isLazy: true, | ||||||
|         }, |         }, | ||||||
|         ((feature, _, __, state) => { |         (feature, _, __, state) => { | ||||||
|             if (Utils.runningFromConsole) { |             if (Utils.runningFromConsole) { | ||||||
|                 // We are running from console, thus probably creating a cache
 |                 // We are running from console, thus probably creating a cache
 | ||||||
|                 // isOpen is irrelevant
 |                 // isOpen is irrelevant
 | ||||||
|  | @ -315,7 +352,7 @@ export default class SimpleMetaTaggers { | ||||||
|             } |             } | ||||||
|             if (feature.properties.opening_hours === "24/7") { |             if (feature.properties.opening_hours === "24/7") { | ||||||
|                 feature.properties._isOpen = "yes" |                 feature.properties._isOpen = "yes" | ||||||
|                 return true; |                 return true | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // _isOpen is calculated dynamically on every call
 |             // _isOpen is calculated dynamically on every call
 | ||||||
|  | @ -325,92 +362,92 @@ export default class SimpleMetaTaggers { | ||||||
|                 get: () => { |                 get: () => { | ||||||
|                     const tags = feature.properties |                     const tags = feature.properties | ||||||
|                     if (tags.opening_hours === undefined) { |                     if (tags.opening_hours === undefined) { | ||||||
|                         return; |                         return | ||||||
|                     } |                     } | ||||||
|                     if (tags._country === undefined) { |                     if (tags._country === undefined) { | ||||||
|                         return; |                         return | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     try { |                     try { | ||||||
|                         const [lon, lat] = GeoOperations.centerpointCoordinates(feature) |                         const [lon, lat] = GeoOperations.centerpointCoordinates(feature) | ||||||
|                         const oh = new opening_hours(tags["opening_hours"], { |                         const oh = new opening_hours( | ||||||
|                             lat: lat, |                             tags["opening_hours"], | ||||||
|                             lon: lon, |                             { | ||||||
|                             address: { |                                 lat: lat, | ||||||
|                                 country_code: tags._country.toLowerCase(), |                                 lon: lon, | ||||||
|                                 state: undefined |                                 address: { | ||||||
|                             } |                                     country_code: tags._country.toLowerCase(), | ||||||
|                         }, <any>{tag_key: "opening_hours"}); |                                     state: undefined, | ||||||
|  |                                 }, | ||||||
|  |                             }, | ||||||
|  |                             <any>{ tag_key: "opening_hours" } | ||||||
|  |                         ) | ||||||
| 
 | 
 | ||||||
|                         // Recalculate!
 |                         // Recalculate!
 | ||||||
|                         return oh.getState() ? "yes" : "no"; |                         return oh.getState() ? "yes" : "no" | ||||||
| 
 |  | ||||||
|                     } catch (e) { |                     } 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 |                         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( |     private static directionSimplified = new SimpleMetaTagger( | ||||||
|         { |         { | ||||||
|             keys: ["_direction:numerical", "_direction:leftright"], |             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 => { |         (feature) => { | ||||||
|             const tags = feature.properties; |             const tags = feature.properties | ||||||
|             const direction = tags["camera:direction"] ?? tags["direction"]; |             const direction = tags["camera:direction"] ?? tags["direction"] | ||||||
|             if (direction === undefined) { |             if (direction === undefined) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|             const n = SimpleMetaTaggers.cardinalDirections[direction] ?? Number(direction); |             const n = SimpleMetaTaggers.cardinalDirections[direction] ?? Number(direction) | ||||||
|             if (isNaN(n)) { |             if (isNaN(n)) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // The % operator has range (-360, 360). We apply a trick to get [0, 360).
 |             // 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:numerical"] = normalized | ||||||
|             tags["_direction:leftright"] = normalized <= 180 ? "right" : "left"; |             tags["_direction:leftright"] = normalized <= 180 ? "right" : "left" | ||||||
|             return true; |             return true | ||||||
|         }) |         } | ||||||
|     ) |     ) | ||||||
|     private static currentTime = new SimpleMetaTagger( |     private static currentTime = new SimpleMetaTagger( | ||||||
|         { |         { | ||||||
|             keys: ["_now:date", "_now:datetime", "_loaded:date", "_loaded:_datetime"], |             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", |             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) => { |         (feature, freshness) => { | ||||||
|             const now = new Date(); |             const now = new Date() | ||||||
| 
 | 
 | ||||||
|             if (typeof freshness === "string") { |             if (typeof freshness === "string") { | ||||||
|                 freshness = new Date(freshness) |                 freshness = new Date(freshness) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             function date(d: Date) { |             function date(d: Date) { | ||||||
|                 return d.toISOString().slice(0, 10); |                 return d.toISOString().slice(0, 10) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             function datetime(d: Date) { |             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:date"] = date(now) | ||||||
|             feature.properties["_now:datetime"] = datetime(now); |             feature.properties["_now:datetime"] = datetime(now) | ||||||
|             feature.properties["_loaded:date"] = date(freshness); |             feature.properties["_loaded:date"] = date(freshness) | ||||||
|             feature.properties["_loaded:datetime"] = datetime(freshness); |             feature.properties["_loaded:datetime"] = datetime(freshness) | ||||||
|             return true; |             return true | ||||||
|         } |         } | ||||||
|     ); |     ) | ||||||
|     public static metatags: SimpleMetaTagger[] = [ |     public static metatags: SimpleMetaTagger[] = [ | ||||||
|         SimpleMetaTaggers.latlon, |         SimpleMetaTaggers.latlon, | ||||||
|         SimpleMetaTaggers.layerInfo, |         SimpleMetaTaggers.layerInfo, | ||||||
|  | @ -424,11 +461,11 @@ export default class SimpleMetaTaggers { | ||||||
|         SimpleMetaTaggers.objectMetaInfo, |         SimpleMetaTaggers.objectMetaInfo, | ||||||
|         SimpleMetaTaggers.noBothButLeftRight, |         SimpleMetaTaggers.noBothButLeftRight, | ||||||
|         SimpleMetaTaggers.geometryType, |         SimpleMetaTaggers.geometryType, | ||||||
|         SimpleMetaTaggers.levels |         SimpleMetaTaggers.levels, | ||||||
| 
 |     ] | ||||||
|     ]; |     public static readonly lazyTags: string[] = [].concat( | ||||||
|     public static readonly lazyTags: string[] = [].concat(...SimpleMetaTaggers.metatags.filter(tagger => tagger.isLazy) |         ...SimpleMetaTaggers.metatags.filter((tagger) => tagger.isLazy).map((tagger) => tagger.keys) | ||||||
|         .map(tagger => tagger.keys)); |     ) | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Edits the given object to rewrite 'both'-tagging into a 'left-right' tagging scheme. |      * 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"]) { |         if (tags["sidewalk"]) { | ||||||
| 
 |  | ||||||
|             const v = tags["sidewalk"] |             const v = tags["sidewalk"] | ||||||
|             switch (v) { |             switch (v) { | ||||||
|                 case "none": |                 case "none": | ||||||
|                 case "no": |                 case "no": | ||||||
|                     set("sidewalk:left", "no"); |                     set("sidewalk:left", "no") | ||||||
|                     set("sidewalk:right", "no"); |                     set("sidewalk:right", "no") | ||||||
|                     break |                     break | ||||||
|                 case "both": |                 case "both": | ||||||
|                     set("sidewalk:left", "yes"); |                     set("sidewalk:left", "yes") | ||||||
|                     set("sidewalk:right", "yes"); |                     set("sidewalk:right", "yes") | ||||||
|                     break; |                     break | ||||||
|                 case "left": |                 case "left": | ||||||
|                     set("sidewalk:left", "yes"); |                     set("sidewalk:left", "yes") | ||||||
|                     set("sidewalk:right", "no"); |                     set("sidewalk:right", "no") | ||||||
|                     break; |                     break | ||||||
|                 case "right": |                 case "right": | ||||||
|                     set("sidewalk:left", "no"); |                     set("sidewalk:left", "no") | ||||||
|                     set("sidewalk:right", "yes"); |                     set("sidewalk:right", "yes") | ||||||
|                     break; |                     break | ||||||
|                 default: |                 default: | ||||||
|                     set("sidewalk:left", v); |                     set("sidewalk:left", v) | ||||||
|                     set("sidewalk:right", v); |                     set("sidewalk:right", v) | ||||||
|                     break; |                     break | ||||||
|             } |             } | ||||||
|             delete tags["sidewalk"] |             delete tags["sidewalk"] | ||||||
|             somethingChanged = true |             somethingChanged = true | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         const regex = /\([^:]*\):both:\(.*\)/ |         const regex = /\([^:]*\):both:\(.*\)/ | ||||||
|         for (const key in tags) { |         for (const key in tags) { | ||||||
|             const v = tags[key] |             const v = tags[key] | ||||||
|  | @ -503,7 +538,6 @@ export default class SimpleMetaTaggers { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         return somethingChanged |         return somethingChanged | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -512,13 +546,16 @@ export default class SimpleMetaTaggers { | ||||||
|             new Combine([ |             new Combine([ | ||||||
|                 "Metatags are extra tags available, in order to display more data or to give better questions.", |                 "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.", |                 "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" |                 "**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") |             ]).SetClass("flex-col"), | ||||||
| 
 |         ] | ||||||
|         ]; |  | ||||||
| 
 | 
 | ||||||
|         subElements.push(new Title("Metatags calculated by MapComplete", 2)) |         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) { |         for (const metatag of SimpleMetaTaggers.metatags) { | ||||||
|             subElements.push( |             subElements.push( | ||||||
|                 new Title(metatag.keys.join(", "), 3), |                 new Title(metatag.keys.join(", "), 3), | ||||||
|  | @ -529,5 +566,4 @@ export default class SimpleMetaTaggers { | ||||||
| 
 | 
 | ||||||
|         return new Combine(subElements).SetClass("flex-col") |         return new Combine(subElements).SetClass("flex-col") | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,89 +1,91 @@ | ||||||
| import FeatureSwitchState from "./FeatureSwitchState"; | import FeatureSwitchState from "./FeatureSwitchState" | ||||||
| import {ElementStorage} from "../ElementStorage"; | import { ElementStorage } from "../ElementStorage" | ||||||
| import {Changes} from "../Osm/Changes"; | import { Changes } from "../Osm/Changes" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc" | ||||||
| import {BBox} from "../BBox"; | import { BBox } from "../BBox" | ||||||
| import {QueryParameters} from "../Web/QueryParameters"; | import { QueryParameters } from "../Web/QueryParameters" | ||||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import ChangeToElementsActor from "../Actors/ChangeToElementsActor"; | import ChangeToElementsActor from "../Actors/ChangeToElementsActor" | ||||||
| import PendingChangesUploader from "../Actors/PendingChangesUploader"; | import PendingChangesUploader from "../Actors/PendingChangesUploader" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The part of the state keeping track of where the elements, loading them, configuring the feature pipeline etc |  * The part of the state keeping track of where the elements, loading them, configuring the feature pipeline etc | ||||||
|  */ |  */ | ||||||
| export default class ElementsState extends FeatureSwitchState { | export default class ElementsState extends FeatureSwitchState { | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      The mapping from id -> UIEventSource<properties> |      The mapping from id -> UIEventSource<properties> | ||||||
|      */ |      */ | ||||||
|     public allElements: ElementStorage = new ElementStorage(); |     public allElements: ElementStorage = new ElementStorage() | ||||||
|   | 
 | ||||||
|     /** |     /** | ||||||
|      The latest element that was selected |      The latest element that was selected | ||||||
|      */ |      */ | ||||||
|     public readonly selectedElement = new UIEventSource<any>( |     public readonly selectedElement = new UIEventSource<any>(undefined, "Selected element") | ||||||
|         undefined, |  | ||||||
|         "Selected element" |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * The map location: currently centered lat, lon and zoom |      * 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 |      * The current visible extent of the screen | ||||||
|      */ |      */ | ||||||
|     public readonly currentBounds = new UIEventSource<BBox>(undefined) |     public readonly currentBounds = new UIEventSource<BBox>(undefined) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     constructor(layoutToUse: LayoutConfig) { |     constructor(layoutToUse: LayoutConfig) { | ||||||
|         super(layoutToUse); |         super(layoutToUse) | ||||||
|          | 
 | ||||||
|          |         function localStorageSynced( | ||||||
|             function localStorageSynced(key: string, deflt: number, docs: string ): UIEventSource<number>{ |             key: string, | ||||||
|                 const localStorage = LocalStorageSource.Get(key) |             deflt: number, | ||||||
|                 const previousValue = localStorage.data |             docs: string | ||||||
|                 const src = UIEventSource.asFloat( |         ): UIEventSource<number> { | ||||||
|                     QueryParameters.GetQueryParameter( |             const localStorage = LocalStorageSource.Get(key) | ||||||
|                         key, |             const previousValue = localStorage.data | ||||||
|                         "" + deflt, |             const src = UIEventSource.asFloat( | ||||||
|                         docs |                 QueryParameters.GetQueryParameter(key, "" + deflt, docs).syncWith(localStorage) | ||||||
|                     ).syncWith(localStorage) |             ) | ||||||
|                 ); | 
 | ||||||
|                  |             if (src.data === deflt) { | ||||||
|                 if(src.data === deflt){ |                 const prev = Number(previousValue) | ||||||
|                     const prev = Number(previousValue) |                 if (!isNaN(prev)) { | ||||||
|                     if(!isNaN(prev)){ |                     src.setData(prev) | ||||||
|                         src.setData(prev) |  | ||||||
|                     } |  | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|                 return src; |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // -- Location control initialization
 |             return src | ||||||
|             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") |  | ||||||
| 
 | 
 | ||||||
|  |         // -- 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" | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|             this.locationControl.setData({ |         this.locationControl.setData({ | ||||||
|                 zoom: Utils.asFloat(zoom.data), |             zoom: Utils.asFloat(zoom.data), | ||||||
|                 lat: Utils.asFloat(lat.data), |             lat: Utils.asFloat(lat.data), | ||||||
|                 lon: Utils.asFloat(lon.data), |             lon: Utils.asFloat(lon.data), | ||||||
|             }) |         }) | ||||||
|             this.locationControl.addCallback((latlonz) => { |         this.locationControl.addCallback((latlonz) => { | ||||||
|                 // Sync the location controls
 |             // Sync the location controls
 | ||||||
|                 zoom.setData(latlonz.zoom); |             zoom.setData(latlonz.zoom) | ||||||
|                 lat.setData(latlonz.lat); |             lat.setData(latlonz.lat) | ||||||
|                 lon.setData(latlonz.lon); |             lon.setData(latlonz.lon) | ||||||
|             }); |         }) | ||||||
| 
 |  | ||||||
|        |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,37 +1,39 @@ | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import FeaturePipeline from "../FeatureSource/FeaturePipeline"; | import FeaturePipeline from "../FeatureSource/FeaturePipeline" | ||||||
| import {Tiles} from "../../Models/TileRange"; | import { Tiles } from "../../Models/TileRange" | ||||||
| import ShowDataLayer from "../../UI/ShowDataLayer/ShowDataLayer"; | import ShowDataLayer from "../../UI/ShowDataLayer/ShowDataLayer" | ||||||
| import {TileHierarchyAggregator} from "../../UI/ShowDataLayer/TileHierarchyAggregator"; | import { TileHierarchyAggregator } from "../../UI/ShowDataLayer/TileHierarchyAggregator" | ||||||
| import ShowTileInfo from "../../UI/ShowDataLayer/ShowTileInfo"; | import ShowTileInfo from "../../UI/ShowDataLayer/ShowTileInfo" | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import MapState from "./MapState"; | import MapState from "./MapState" | ||||||
| import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"; | import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler" | ||||||
| import Hash from "../Web/Hash"; | import Hash from "../Web/Hash" | ||||||
| import {BBox} from "../BBox"; | import { BBox } from "../BBox" | ||||||
| import FeatureInfoBox from "../../UI/Popup/FeatureInfoBox"; | import FeatureInfoBox from "../../UI/Popup/FeatureInfoBox" | ||||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource"; | import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" | ||||||
| import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator"; | import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator" | ||||||
| import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"; | import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen" | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
| 
 | 
 | ||||||
| export default class FeaturePipelineState extends MapState { | export default class FeaturePipelineState extends MapState { | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * The piece of code which fetches data from various sources and shows it on the background map |      * The piece of code which fetches data from various sources and shows it on the background map | ||||||
|      */ |      */ | ||||||
|     public readonly featurePipeline: FeaturePipeline; |     public readonly featurePipeline: FeaturePipeline | ||||||
|     private readonly featureAggregator: TileHierarchyAggregator; |     private readonly featureAggregator: TileHierarchyAggregator | ||||||
|     private readonly metatagRecalculator: MetaTagRecalculator |     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) { |     constructor(layoutToUse: LayoutConfig) { | ||||||
|         super(layoutToUse); |         super(layoutToUse) | ||||||
| 
 | 
 | ||||||
|         const clustering = layoutToUse?.clustering |         const clustering = layoutToUse?.clustering | ||||||
|         this.featureAggregator = TileHierarchyAggregator.createHierarchy(this); |         this.featureAggregator = TileHierarchyAggregator.createHierarchy(this) | ||||||
|         const clusterCounter = this.featureAggregator |         const clusterCounter = this.featureAggregator | ||||||
|         const self = this; |         const self = this | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|          * We are a bit in a bind: |          * We are a bit in a bind: | ||||||
|  | @ -51,26 +53,26 @@ export default class FeaturePipelineState extends MapState { | ||||||
|                 self.metatagRecalculator.registerSource(source) |                 self.metatagRecalculator.registerSource(source) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |  | ||||||
|          |  | ||||||
|         function registerSource(source: FeatureSourceForLayer & Tiled) { |  | ||||||
| 
 | 
 | ||||||
|  |         function registerSource(source: FeatureSourceForLayer & Tiled) { | ||||||
|             clusterCounter.addTile(source) |             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
 |             // Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering
 | ||||||
|             const doShowFeatures = source.features.map( |             const doShowFeatures = source.features.map( | ||||||
|                 f => { |                 (f) => { | ||||||
|                     const z = self.locationControl.data.zoom |                     const z = self.locationControl.data.zoom | ||||||
| 
 | 
 | ||||||
|                     if (!source.layer.isDisplayed.data) { |                     if (!source.layer.isDisplayed.data) { | ||||||
|                         return false; |                         return false | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     const bounds = self.currentBounds.data |                     const bounds = self.currentBounds.data | ||||||
|                     if (bounds === undefined) { |                     if (bounds === undefined) { | ||||||
|                         // Map is not yet displayed
 |                         // Map is not yet displayed
 | ||||||
|                         return false; |                         return false | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     if (!sourceBBox.data.overlapsWith(bounds)) { |                     if (!sourceBBox.data.overlapsWith(bounds)) { | ||||||
|  | @ -78,10 +80,9 @@ export default class FeaturePipelineState extends MapState { | ||||||
|                         return false |                         return false | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|                     if (z < source.layer.layerDef.minzoom) { |                     if (z < source.layer.layerDef.minzoom) { | ||||||
|                         // Layer is always hidden for this zoom level
 |                         // Layer is always hidden for this zoom level
 | ||||||
|                         return false; |                         return false | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     if (z > clustering.maxZoom) { |                     if (z > clustering.maxZoom) { | ||||||
|  | @ -93,55 +94,55 @@ export default class FeaturePipelineState extends MapState { | ||||||
|                         return false |                         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) { |                     if (tileZ >= z) { | ||||||
| 
 |  | ||||||
|                         while (tileZ > z) { |                         while (tileZ > z) { | ||||||
|                             tileZ-- |                             tileZ-- | ||||||
|                             tileX = Math.floor(tileX / 2) |                             tileX = Math.floor(tileX / 2) | ||||||
|                             tileY = Math.floor(tileY / 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
 |                             // To much elements
 | ||||||
|                             return false |                             return false | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|                     return true |                     return true | ||||||
|                 }, [self.currentBounds, source.layer.isDisplayed, sourceBBox] |                 }, | ||||||
|  |                 [self.currentBounds, source.layer.isDisplayed, sourceBBox] | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             new ShowDataLayer( |             new ShowDataLayer({ | ||||||
|                 { |                 features: source, | ||||||
|                     features: source, |                 leafletMap: self.leafletMap, | ||||||
|                     leafletMap: self.leafletMap, |                 layerToShow: source.layer.layerDef, | ||||||
|                     layerToShow: source.layer.layerDef, |                 doShowLayer: doShowFeatures, | ||||||
|                     doShowLayer: doShowFeatures, |                 selectedElement: self.selectedElement, | ||||||
|                     selectedElement: self.selectedElement, |                 state: self, | ||||||
|                     state: self, |                 popup: (tags, layer) => self.CreatePopup(tags, layer), | ||||||
|                     popup: (tags, layer) => self.CreatePopup(tags, layer) |             }) | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |         this.featurePipeline = new FeaturePipeline(registerSource, this, { | ||||||
|         this.featurePipeline = new FeaturePipeline(registerSource, this, {handleRawFeatureSource: registerRaw}); |             handleRawFeatureSource: registerRaw, | ||||||
|  |         }) | ||||||
|         this.metatagRecalculator = new MetaTagRecalculator(this, this.featurePipeline) |         this.metatagRecalculator = new MetaTagRecalculator(this, this.featurePipeline) | ||||||
|         this.metatagRecalculator.registerSource(this.currentView, true) |         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) |         new SelectedFeatureHandler(Hash.hash, this) | ||||||
| 
 | 
 | ||||||
|         this.AddClusteringToMap(this.leafletMap) |         this.AddClusteringToMap(this.leafletMap) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     public CreatePopup(tags:UIEventSource<any> , layer: LayerConfig): ScrollableFullScreen{ |     public CreatePopup(tags: UIEventSource<any>, layer: LayerConfig): ScrollableFullScreen { | ||||||
|         if(this.popups.has(tags.data.id)){ |         if (this.popups.has(tags.data.id)) { | ||||||
|            return this.popups.get(tags.data.id) |             return this.popups.get(tags.data.id) | ||||||
|         } |         } | ||||||
|         const popup = new FeatureInfoBox(tags, layer, this) |         const popup = new FeatureInfoBox(tags, layer, this) | ||||||
|         this.popups.set(tags.data.id, popup) |         this.popups.set(tags.data.id, popup) | ||||||
|  | @ -155,15 +156,19 @@ export default class FeaturePipelineState extends MapState { | ||||||
|      */ |      */ | ||||||
|     public AddClusteringToMap(leafletMap: UIEventSource<any>) { |     public AddClusteringToMap(leafletMap: UIEventSource<any>) { | ||||||
|         const clustering = this.layoutToUse.clustering |         const clustering = this.layoutToUse.clustering | ||||||
|         const self = this; |         const self = this | ||||||
|         new ShowDataLayer({ |         new ShowDataLayer({ | ||||||
|             features: this.featureAggregator.getCountsForZoom(clustering, this.locationControl, clustering.minNeededElements), |             features: this.featureAggregator.getCountsForZoom( | ||||||
|  |                 clustering, | ||||||
|  |                 this.locationControl, | ||||||
|  |                 clustering.minNeededElements | ||||||
|  |             ), | ||||||
|             leafletMap: leafletMap, |             leafletMap: leafletMap, | ||||||
|             layerToShow: ShowTileInfo.styling, |             layerToShow: ShowTileInfo.styling, | ||||||
|             popup: this.featureSwitchIsDebugging.data ? (tags, layer) => new FeatureInfoBox(tags, layer, self) : undefined, |             popup: this.featureSwitchIsDebugging.data | ||||||
|             state: this |                 ? (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 |  * 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 LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import {QueryParameters} from "../Web/QueryParameters"; | import { QueryParameters } from "../Web/QueryParameters" | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| export default class FeatureSwitchState { | export default class FeatureSwitchState { | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * The layout that is being used in this run |      * The layout that is being used in this run | ||||||
|      */ |      */ | ||||||
|     public readonly layoutToUse: LayoutConfig; |     public readonly layoutToUse: LayoutConfig | ||||||
| 
 | 
 | ||||||
|     public readonly featureSwitchUserbadge: UIEventSource<boolean>; |     public readonly featureSwitchUserbadge: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchSearch: UIEventSource<boolean>; |     public readonly featureSwitchSearch: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchBackgroundSelection: UIEventSource<boolean>; |     public readonly featureSwitchBackgroundSelection: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchAddNew: UIEventSource<boolean>; |     public readonly featureSwitchAddNew: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>; |     public readonly featureSwitchWelcomeMessage: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchExtraLinkEnabled: UIEventSource<boolean>; |     public readonly featureSwitchExtraLinkEnabled: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchMoreQuests: UIEventSource<boolean>; |     public readonly featureSwitchMoreQuests: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchShareScreen: UIEventSource<boolean>; |     public readonly featureSwitchShareScreen: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchGeolocation: UIEventSource<boolean>; |     public readonly featureSwitchGeolocation: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchIsTesting: UIEventSource<boolean>; |     public readonly featureSwitchIsTesting: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchIsDebugging: UIEventSource<boolean>; |     public readonly featureSwitchIsDebugging: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>; |     public readonly featureSwitchShowAllQuestions: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchApiURL: UIEventSource<string>; |     public readonly featureSwitchApiURL: UIEventSource<string> | ||||||
|     public readonly featureSwitchFilter: UIEventSource<boolean>; |     public readonly featureSwitchFilter: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchEnableExport: UIEventSource<boolean>; |     public readonly featureSwitchEnableExport: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchFakeUser: UIEventSource<boolean>; |     public readonly featureSwitchFakeUser: UIEventSource<boolean> | ||||||
|     public readonly featureSwitchExportAsPdf: UIEventSource<boolean>; |     public readonly featureSwitchExportAsPdf: UIEventSource<boolean> | ||||||
|     public readonly overpassUrl: UIEventSource<string[]>; |     public readonly overpassUrl: UIEventSource<string[]> | ||||||
|     public readonly overpassTimeout: UIEventSource<number>; |     public readonly overpassTimeout: UIEventSource<number> | ||||||
|     public readonly overpassMaxZoom: UIEventSource<number>; |     public readonly overpassMaxZoom: UIEventSource<number> | ||||||
|     public readonly osmApiTileSize: UIEventSource<number>; |     public readonly osmApiTileSize: UIEventSource<number> | ||||||
|     public readonly backgroundLayerId: UIEventSource<string>; |     public readonly backgroundLayerId: UIEventSource<string> | ||||||
| 
 | 
 | ||||||
|     public constructor(layoutToUse: LayoutConfig) { |     public constructor(layoutToUse: LayoutConfig) { | ||||||
|         this.layoutToUse = layoutToUse; |         this.layoutToUse = layoutToUse | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|         // Helper function to initialize feature switches
 |         // Helper function to initialize feature switches
 | ||||||
|         function featSw( |         function featSw( | ||||||
|  | @ -47,104 +45,104 @@ export default class FeatureSwitchState { | ||||||
|             deflt: (layout: LayoutConfig) => boolean, |             deflt: (layout: LayoutConfig) => boolean, | ||||||
|             documentation: string |             documentation: string | ||||||
|         ): UIEventSource<boolean> { |         ): UIEventSource<boolean> { | ||||||
| 
 |             const defaultValue = deflt(layoutToUse) | ||||||
|             const defaultValue = deflt(layoutToUse); |  | ||||||
|             const queryParam = QueryParameters.GetQueryParameter( |             const queryParam = QueryParameters.GetQueryParameter( | ||||||
|                 key, |                 key, | ||||||
|                 "" + defaultValue, |                 "" + defaultValue, | ||||||
|                 documentation |                 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( |         this.featureSwitchUserbadge = featSw( | ||||||
|             "fs-userbadge", |             "fs-userbadge", | ||||||
|             (layoutToUse) => layoutToUse?.enableUserBadge ?? true, |             (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." |             "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( |         this.featureSwitchSearch = featSw( | ||||||
|             "fs-search", |             "fs-search", | ||||||
|             (layoutToUse) => layoutToUse?.enableSearch ?? true, |             (layoutToUse) => layoutToUse?.enableSearch ?? true, | ||||||
|             "Disables/Enables the search bar" |             "Disables/Enables the search bar" | ||||||
|         ); |         ) | ||||||
|         this.featureSwitchBackgroundSelection = featSw( |         this.featureSwitchBackgroundSelection = featSw( | ||||||
|             "fs-background", |             "fs-background", | ||||||
|             (layoutToUse) => layoutToUse?.enableBackgroundLayerSelection ?? true, |             (layoutToUse) => layoutToUse?.enableBackgroundLayerSelection ?? true, | ||||||
|             "Disables/Enables the background layer control" |             "Disables/Enables the background layer control" | ||||||
|         ); |         ) | ||||||
| 
 | 
 | ||||||
|         this.featureSwitchFilter = featSw( |         this.featureSwitchFilter = featSw( | ||||||
|             "fs-filter", |             "fs-filter", | ||||||
|             (layoutToUse) => layoutToUse?.enableLayers ?? true, |             (layoutToUse) => layoutToUse?.enableLayers ?? true, | ||||||
|             "Disables/Enables the filter view" |             "Disables/Enables the filter view" | ||||||
|         ); |         ) | ||||||
|         this.featureSwitchAddNew = featSw( |         this.featureSwitchAddNew = featSw( | ||||||
|             "fs-add-new", |             "fs-add-new", | ||||||
|             (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true, |             (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true, | ||||||
|             "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)" |             "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)" | ||||||
|         ); |         ) | ||||||
|         this.featureSwitchWelcomeMessage = featSw( |         this.featureSwitchWelcomeMessage = featSw( | ||||||
|             "fs-welcome-message", |             "fs-welcome-message", | ||||||
|             () => true, |             () => true, | ||||||
|             "Disables/enables the help menu or welcome message" |             "Disables/enables the help menu or welcome message" | ||||||
|         ); |         ) | ||||||
|         this.featureSwitchExtraLinkEnabled = featSw( |         this.featureSwitchExtraLinkEnabled = featSw( | ||||||
|             "fs-iframe-popout", |             "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)" |             "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( |         this.featureSwitchMoreQuests = featSw( | ||||||
|             "fs-more-quests", |             "fs-more-quests", | ||||||
|             (layoutToUse) => layoutToUse?.enableMoreQuests ?? true, |             (layoutToUse) => layoutToUse?.enableMoreQuests ?? true, | ||||||
|             "Disables/Enables the 'More Quests'-tab in the welcome message" |             "Disables/Enables the 'More Quests'-tab in the welcome message" | ||||||
|         ); |         ) | ||||||
|         this.featureSwitchShareScreen = featSw( |         this.featureSwitchShareScreen = featSw( | ||||||
|             "fs-share-screen", |             "fs-share-screen", | ||||||
|             (layoutToUse) => layoutToUse?.enableShareScreen ?? true, |             (layoutToUse) => layoutToUse?.enableShareScreen ?? true, | ||||||
|             "Disables/Enables the 'Share-screen'-tab in the welcome message" |             "Disables/Enables the 'Share-screen'-tab in the welcome message" | ||||||
|         ); |         ) | ||||||
|         this.featureSwitchGeolocation = featSw( |         this.featureSwitchGeolocation = featSw( | ||||||
|             "fs-geolocation", |             "fs-geolocation", | ||||||
|             (layoutToUse) => layoutToUse?.enableGeolocation ?? true, |             (layoutToUse) => layoutToUse?.enableGeolocation ?? true, | ||||||
|             "Disables/Enables the geolocation button" |             "Disables/Enables the geolocation button" | ||||||
|         ); |         ) | ||||||
|         this.featureSwitchShowAllQuestions = featSw( |         this.featureSwitchShowAllQuestions = featSw( | ||||||
|             "fs-all-questions", |             "fs-all-questions", | ||||||
|             (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false, |             (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false, | ||||||
|             "Always show all questions" |             "Always show all questions" | ||||||
|         ); |         ) | ||||||
| 
 | 
 | ||||||
|         this.featureSwitchEnableExport = featSw( |         this.featureSwitchEnableExport = featSw( | ||||||
|             "fs-export", |             "fs-export", | ||||||
|             (layoutToUse) => layoutToUse?.enableExportButton ?? false, |             (layoutToUse) => layoutToUse?.enableExportButton ?? false, | ||||||
|             "Enable the export as GeoJSON and CSV button" |             "Enable the export as GeoJSON and CSV button" | ||||||
|         ); |         ) | ||||||
|         this.featureSwitchExportAsPdf = featSw( |         this.featureSwitchExportAsPdf = featSw( | ||||||
|             "fs-pdf", |             "fs-pdf", | ||||||
|             (layoutToUse) => layoutToUse?.enablePdfDownload ?? false, |             (layoutToUse) => layoutToUse?.enablePdfDownload ?? false, | ||||||
|             "Enable the PDF download button" |             "Enable the PDF download button" | ||||||
|         ); |         ) | ||||||
| 
 | 
 | ||||||
|         this.featureSwitchApiURL = QueryParameters.GetQueryParameter( |         this.featureSwitchApiURL = QueryParameters.GetQueryParameter( | ||||||
|             "backend", |             "backend", | ||||||
|             "osm", |             "osm", | ||||||
|             "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'" |             "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'" | ||||||
|         ); |         ) | ||||||
| 
 | 
 | ||||||
| 
 |         let testingDefaultValue = false | ||||||
|         let testingDefaultValue = false; |         if ( | ||||||
|         if (this.featureSwitchApiURL.data !== "osm-test" && !Utils.runningFromConsole && |             this.featureSwitchApiURL.data !== "osm-test" && | ||||||
|             (location.hostname === "localhost" || location.hostname === "127.0.0.1")) { |             !Utils.runningFromConsole && | ||||||
|  |             (location.hostname === "localhost" || location.hostname === "127.0.0.1") | ||||||
|  |         ) { | ||||||
|             testingDefaultValue = true |             testingDefaultValue = true | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         this.featureSwitchIsTesting = QueryParameters.GetBooleanQueryParameter( |         this.featureSwitchIsTesting = QueryParameters.GetBooleanQueryParameter( | ||||||
|             "test", |             "test", | ||||||
|             testingDefaultValue, |             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" |             "If true, shows some extra debugging help such as all the available tags on every object" | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter("fake-user", false, |         this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter( | ||||||
|             "If true, 'dryrun' mode is activated and a fake user account is loaded") |             "fake-user", | ||||||
|  |             false, | ||||||
|  |             "If true, 'dryrun' mode is activated and a fake user account is loaded" | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
| 
 |         this.overpassUrl = QueryParameters.GetQueryParameter( | ||||||
|         this.overpassUrl = QueryParameters.GetQueryParameter("overpassUrl", |             "overpassUrl", | ||||||
|             (layoutToUse?.overpassUrl ?? Constants.defaultOverpassUrls).join(","), |             (layoutToUse?.overpassUrl ?? Constants.defaultOverpassUrls).join(","), | ||||||
|             "Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter" |             "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( | ||||||
|             "" + layoutToUse?.overpassTimeout, |             QueryParameters.GetQueryParameter( | ||||||
|             "Set a different timeout (in seconds) for queries in overpass")) |                 "overpassTimeout", | ||||||
|  |                 "" + layoutToUse?.overpassTimeout, | ||||||
|  |                 "Set a different timeout (in seconds) for queries in overpass" | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
| 
 |         this.overpassMaxZoom = UIEventSource.asFloat( | ||||||
|         this.overpassMaxZoom = |             QueryParameters.GetQueryParameter( | ||||||
|             UIEventSource.asFloat(QueryParameters.GetQueryParameter("overpassMaxZoom", |                 "overpassMaxZoom", | ||||||
|                 "" + layoutToUse?.overpassMaxZoom, |                 "" + layoutToUse?.overpassMaxZoom, | ||||||
|                 " point to switch between OSM-api and overpass")) |                 " point to switch between OSM-api and overpass" | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         this.osmApiTileSize = |         this.osmApiTileSize = UIEventSource.asFloat( | ||||||
|             UIEventSource.asFloat(QueryParameters.GetQueryParameter("osmApiTileSize", |             QueryParameters.GetQueryParameter( | ||||||
|  |                 "osmApiTileSize", | ||||||
|                 "" + layoutToUse?.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) { |             if (!userbadge) { | ||||||
|                 this.featureSwitchAddNew.setData(false) |                 this.featureSwitchAddNew.setData(false) | ||||||
|             } |             } | ||||||
|  | @ -191,9 +205,6 @@ export default class FeatureSwitchState { | ||||||
|             "background", |             "background", | ||||||
|             layoutToUse?.defaultBackgroundId ?? "osm", |             layoutToUse?.defaultBackgroundId ?? "osm", | ||||||
|             "The id of the background layer to start with" |             "The id of the background layer to start with" | ||||||
|         ); |         ) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,34 +1,33 @@ | ||||||
| import UserRelatedState from "./UserRelatedState"; | import UserRelatedState from "./UserRelatedState" | ||||||
| import {Store, Stores, UIEventSource} from "../UIEventSource"; | import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||||
| import BaseLayer from "../../Models/BaseLayer"; | import BaseLayer from "../../Models/BaseLayer" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import AvailableBaseLayers from "../Actors/AvailableBaseLayers"; | import AvailableBaseLayers from "../Actors/AvailableBaseLayers" | ||||||
| import Attribution from "../../UI/BigComponents/Attribution"; | import Attribution from "../../UI/BigComponents/Attribution" | ||||||
| import Minimap, {MinimapObj} from "../../UI/Base/Minimap"; | import Minimap, { MinimapObj } from "../../UI/Base/Minimap" | ||||||
| import {Tiles} from "../../Models/TileRange"; | import { Tiles } from "../../Models/TileRange" | ||||||
| import BaseUIElement from "../../UI/BaseUIElement"; | import BaseUIElement from "../../UI/BaseUIElement" | ||||||
| import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; | import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" | ||||||
| import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; | import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" | ||||||
| import {QueryParameters} from "../Web/QueryParameters"; | import { QueryParameters } from "../Web/QueryParameters" | ||||||
| import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"; | import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer" | ||||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource"; | import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" | ||||||
| import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"; | import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource" | ||||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||||
| import {GeoOperations} from "../GeoOperations"; | import { GeoOperations } from "../GeoOperations" | ||||||
| import TitleHandler from "../Actors/TitleHandler"; | import TitleHandler from "../Actors/TitleHandler" | ||||||
| import {BBox} from "../BBox"; | import { BBox } from "../BBox" | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
| import {TiledStaticFeatureSource} from "../FeatureSource/Sources/StaticFeatureSource"; | import { TiledStaticFeatureSource } from "../FeatureSource/Sources/StaticFeatureSource" | ||||||
| import {Translation, TypedTranslation} from "../../UI/i18n/Translation"; | import { Translation, TypedTranslation } from "../../UI/i18n/Translation" | ||||||
| import {Tag} from "../Tags/Tag"; | import { Tag } from "../Tags/Tag" | ||||||
| import {OsmConnection} from "../Osm/OsmConnection"; | import { OsmConnection } from "../Osm/OsmConnection" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| export interface GlobalFilter { | export interface GlobalFilter { | ||||||
|     filter: FilterState, |     filter: FilterState | ||||||
|     id: string, |     id: string | ||||||
|     onNewPoint: { |     onNewPoint: { | ||||||
|         safetyCheck: Translation, |         safetyCheck: Translation | ||||||
|         confirmAddNew: TypedTranslation<{ preset: Translation }> |         confirmAddNew: TypedTranslation<{ preset: Translation }> | ||||||
|         tags: Tag[] |         tags: Tag[] | ||||||
|     } |     } | ||||||
|  | @ -38,60 +37,64 @@ export interface GlobalFilter { | ||||||
|  * Contains all the leaflet-map related state |  * Contains all the leaflet-map related state | ||||||
|  */ |  */ | ||||||
| export default class MapState extends UserRelatedState { | export default class MapState extends UserRelatedState { | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      The leaflet instance of the big basemap |      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 |      * A list of currently available background layers | ||||||
|      */ |      */ | ||||||
|     public availableBackgroundLayers: Store<BaseLayer[]>; |     public availableBackgroundLayers: Store<BaseLayer[]> | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * The current background layer |      * The current background layer | ||||||
|      */ |      */ | ||||||
|     public backgroundLayer: UIEventSource<BaseLayer>; |     public backgroundLayer: UIEventSource<BaseLayer> | ||||||
|     /** |     /** | ||||||
|      * Last location where a click was registered |      * Last location where a click was registered | ||||||
|      */ |      */ | ||||||
|     public readonly LastClickLocation: UIEventSource<{ |     public readonly LastClickLocation: UIEventSource<{ | ||||||
|         lat: number; |         lat: number | ||||||
|         lon: number; |         lon: number | ||||||
|     }> = new UIEventSource<{ lat: number; lon: number }>(undefined); |     }> = new UIEventSource<{ lat: number; lon: number }>(undefined) | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * The bounds of the current map view |      * The bounds of the current map view | ||||||
|      */ |      */ | ||||||
|     public currentView: FeatureSourceForLayer & Tiled; |     public currentView: FeatureSourceForLayer & Tiled | ||||||
|     /** |     /** | ||||||
|      * The location as delivered by the GPS |      * The location as delivered by the GPS | ||||||
|      */ |      */ | ||||||
|     public currentUserLocation: SimpleFeatureSource; |     public currentUserLocation: SimpleFeatureSource | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * All previously visited points |      * All previously visited points | ||||||
|      */ |      */ | ||||||
|     public historicalUserLocations: SimpleFeatureSource; |     public historicalUserLocations: SimpleFeatureSource | ||||||
|     /** |     /** | ||||||
|      * The number of seconds that the GPS-locations are stored in memory. |      * The number of seconds that the GPS-locations are stored in memory. | ||||||
|      * Time in seconds |      * Time in seconds | ||||||
|      */ |      */ | ||||||
|     public gpsLocationHistoryRetentionTime = new UIEventSource(7 * 24 * 60 * 60, "gps_location_retention") |     public gpsLocationHistoryRetentionTime = new UIEventSource( | ||||||
|     public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled; |         7 * 24 * 60 * 60, | ||||||
|  |         "gps_location_retention" | ||||||
|  |     ) | ||||||
|  |     public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * A feature source containing the current home location of the user |      * A feature source containing the current home location of the user | ||||||
|      */ |      */ | ||||||
|     public homeLocation: FeatureSourceForLayer & Tiled |     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 |      * 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 |      * Filters which apply onto all layers | ||||||
|  | @ -101,31 +104,30 @@ export default class MapState extends UserRelatedState { | ||||||
|     /** |     /** | ||||||
|      * Which overlays are shown |      * 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 }) { |     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 |         let defaultLayer = AvailableBaseLayers.osmCarto | ||||||
|         const available = this.availableBackgroundLayers.data; |         const available = this.availableBackgroundLayers.data | ||||||
|         for (const layer of available) { |         for (const layer of available) { | ||||||
|             if (this.backgroundLayerId.data === layer.id) { |             if (this.backgroundLayerId.data === layer.id) { | ||||||
|                 defaultLayer = layer; |                 defaultLayer = layer | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         const self = this |         const self = this | ||||||
|         this.backgroundLayer = new UIEventSource<BaseLayer>(defaultLayer) |         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( |         const attr = new Attribution( | ||||||
|             this.locationControl, |             this.locationControl, | ||||||
|             this.osmConnection.userDetails, |             this.osmConnection.userDetails, | ||||||
|             this.layoutToUse, |             this.layoutToUse, | ||||||
|             this.currentBounds |             this.currentBounds | ||||||
|         ); |         ) | ||||||
| 
 | 
 | ||||||
|         // Will write into this.leafletMap
 |         // Will write into this.leafletMap
 | ||||||
|         this.mainMapObject = Minimap.createMiniMap({ |         this.mainMapObject = Minimap.createMiniMap({ | ||||||
|  | @ -134,18 +136,23 @@ export default class MapState extends UserRelatedState { | ||||||
|             leafletMap: this.leafletMap, |             leafletMap: this.leafletMap, | ||||||
|             bounds: this.currentBounds, |             bounds: this.currentBounds, | ||||||
|             attribution: attr, |             attribution: attr, | ||||||
|             lastClickLocation: this.LastClickLocation |             lastClickLocation: this.LastClickLocation, | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 |         this.overlayToggles = | ||||||
|         this.overlayToggles = this.layoutToUse?.tileLayerSources |             this.layoutToUse?.tileLayerSources | ||||||
|             ?.filter(c => c.name !== undefined) |                 ?.filter((c) => c.name !== undefined) | ||||||
|             ?.map(c => ({ |                 ?.map((c) => ({ | ||||||
|                 config: 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, | ||||||
|         this.filteredLayers = new UIEventSource<FilteredLayer[]>( MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection)) |                         c.defaultState, | ||||||
| 
 |                         "Wether or not the overlay " + c.id + " is shown" | ||||||
|  |                     ), | ||||||
|  |                 })) ?? [] | ||||||
|  |         this.filteredLayers = new UIEventSource<FilteredLayer[]>( | ||||||
|  |             MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection) | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         this.lockBounds() |         this.lockBounds() | ||||||
|         this.AddAllOverlaysToMap(this.leafletMap) |         this.AddAllOverlaysToMap(this.leafletMap) | ||||||
|  | @ -155,7 +162,7 @@ export default class MapState extends UserRelatedState { | ||||||
|         this.initUserLocationTrail() |         this.initUserLocationTrail() | ||||||
|         this.initCurrentView() |         this.initCurrentView() | ||||||
| 
 | 
 | ||||||
|         new TitleHandler(this); |         new TitleHandler(this) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) { |     public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) { | ||||||
|  | @ -171,15 +178,14 @@ export default class MapState extends UserRelatedState { | ||||||
|             } |             } | ||||||
|             new ShowOverlayLayer(tileLayerSource, leafletMap) |             new ShowOverlayLayer(tileLayerSource, leafletMap) | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private lockBounds() { |     private lockBounds() { | ||||||
|         const layout = this.layoutToUse; |         const layout = this.layoutToUse | ||||||
|         if (!layout?.lockLocation) { |         if (!layout?.lockLocation) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         console.warn("Locking the bounds to ", layout.lockLocation); |         console.warn("Locking the bounds to ", layout.lockLocation) | ||||||
|         this.mainMapObject.installBounds( |         this.mainMapObject.installBounds( | ||||||
|             new BBox(layout.lockLocation), |             new BBox(layout.lockLocation), | ||||||
|             this.featureSwitchIsTesting.data |             this.featureSwitchIsTesting.data | ||||||
|  | @ -187,69 +193,82 @@ export default class MapState extends UserRelatedState { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private initCurrentView() { |     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) { |         if (currentViewLayer === undefined) { | ||||||
|             // This layer is not needed by the theme and thus unloaded
 |             // This layer is not needed by the theme and thus unloaded
 | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         let i = 0 |         let i = 0 | ||||||
|         const self = this; |         const self = this | ||||||
|         const features: Store<{ feature: any, freshness: Date }[]> = this.currentBounds.map(bounds => { |         const features: Store<{ feature: any; freshness: Date }[]> = this.currentBounds.map( | ||||||
|             if (bounds === undefined) { |             (bounds) => { | ||||||
|                 return [] |                 if (bounds === undefined) { | ||||||
|             } |                     return [] | ||||||
|             i++ |  | ||||||
|             const feature = { |  | ||||||
|                 freshness: new Date(), |  | ||||||
|                 feature: { |  | ||||||
|                     type: "Feature", |  | ||||||
|                     properties: { |  | ||||||
|                         id: "current_view-" + i, |  | ||||||
|                         "current_view": "yes", |  | ||||||
|                         "zoom": "" + self.locationControl.data.zoom |  | ||||||
|                     }, |  | ||||||
|                     geometry: { |  | ||||||
|                         type: "Polygon", |  | ||||||
|                         coordinates: [[ |  | ||||||
|                             [bounds.maxLon, bounds.maxLat], |  | ||||||
|                             [bounds.minLon, bounds.maxLat], |  | ||||||
|                             [bounds.minLon, bounds.minLat], |  | ||||||
|                             [bounds.maxLon, bounds.minLat], |  | ||||||
|                             [bounds.maxLon, bounds.maxLat], |  | ||||||
|                         ]] |  | ||||||
|                     } |  | ||||||
|                 } |                 } | ||||||
|  |                 i++ | ||||||
|  |                 const feature = { | ||||||
|  |                     freshness: new Date(), | ||||||
|  |                     feature: { | ||||||
|  |                         type: "Feature", | ||||||
|  |                         properties: { | ||||||
|  |                             id: "current_view-" + i, | ||||||
|  |                             current_view: "yes", | ||||||
|  |                             zoom: "" + self.locationControl.data.zoom, | ||||||
|  |                         }, | ||||||
|  |                         geometry: { | ||||||
|  |                             type: "Polygon", | ||||||
|  |                             coordinates: [ | ||||||
|  |                                 [ | ||||||
|  |                                     [bounds.maxLon, bounds.maxLat], | ||||||
|  |                                     [bounds.minLon, bounds.maxLat], | ||||||
|  |                                     [bounds.minLon, bounds.minLat], | ||||||
|  |                                     [bounds.maxLon, bounds.minLat], | ||||||
|  |                                     [bounds.maxLon, bounds.maxLat], | ||||||
|  |                                 ], | ||||||
|  |                             ], | ||||||
|  |                         }, | ||||||
|  |                     }, | ||||||
|  |                 } | ||||||
|  |                 return [feature] | ||||||
|             } |             } | ||||||
|             return [feature] |         ) | ||||||
|         }) |  | ||||||
| 
 | 
 | ||||||
|         this.currentView = new TiledStaticFeatureSource(features, currentViewLayer); |         this.currentView = new TiledStaticFeatureSource(features, currentViewLayer) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private initGpsLocation() { |     private initGpsLocation() { | ||||||
|         // Initialize the gps layer data. This is emtpy for now, the actual writing happens in the Geolocationhandler
 |         // 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) { |         if (gpsLayerDef === undefined) { | ||||||
|             return |             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() { |     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() |         const now = new Date().getTime() | ||||||
|         features.data = features.data |         features.data = features.data | ||||||
|             .map(ff => ({feature: ff.feature, freshness: new Date(ff.freshness)})) |             .map((ff) => ({ feature: ff.feature, freshness: new Date(ff.freshness) })) | ||||||
|             .filter(ff => (now - ff.freshness.getTime()) < 1000 * this.gpsLocationHistoryRetentionTime.data) |             .filter( | ||||||
|  |                 (ff) => | ||||||
|  |                     now - ff.freshness.getTime() < 1000 * this.gpsLocationHistoryRetentionTime.data | ||||||
|  |             ) | ||||||
|         features.ping() |         features.ping() | ||||||
|         const self = this; |         const self = this | ||||||
|         let i = 0 |         let i = 0 | ||||||
|         this.currentUserLocation?.features?.addCallbackAndRunD(([location]) => { |         this.currentUserLocation?.features?.addCallbackAndRunD(([location]) => { | ||||||
|             if (location === undefined) { |             if (location === undefined) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const previousLocation = features.data[features.data.length - 1] |             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
 |                 let timeDiff = Number.MAX_VALUE // in seconds
 | ||||||
|                 const olderLocation = features.data[features.data.length - 2] |                 const olderLocation = features.data[features.data.length - 2] | ||||||
|                 if (olderLocation !== undefined) { |                 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) { |                 if (d < 20 && timeDiff < 60) { | ||||||
|                     // Do not append changes less then 20m - it's probably noise anyway
 |                     // Do not append changes less then 20m - it's probably noise anyway
 | ||||||
|                     return; |                     return | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const feature = JSON.parse(JSON.stringify(location.feature)) |             const feature = JSON.parse(JSON.stringify(location.feature)) | ||||||
|             feature.properties.id = "gps/" + features.data.length |             feature.properties.id = "gps/" + features.data.length | ||||||
|             i++ |             i++ | ||||||
|             features.data.push({feature, freshness: new Date()}) |             features.data.push({ feature, freshness: new Date() }) | ||||||
|             features.ping() |             features.ping() | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 |         let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter( | ||||||
|         let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_location_history")[0] |             (l) => l.layerDef.id === "gps_location_history" | ||||||
|  |         )[0] | ||||||
|         if (gpsLayerDef !== undefined) { |         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) |             this.changes.setHistoricalUserLocations(this.historicalUserLocations) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |         const asLine = features.map((allPoints) => { | ||||||
|         const asLine = features.map(allPoints => { |  | ||||||
|             if (allPoints === undefined || allPoints.length < 2) { |             if (allPoints === undefined || allPoints.length < 2) { | ||||||
|                 return [] |                 return [] | ||||||
|             } |             } | ||||||
|  | @ -292,136 +318,184 @@ export default class MapState extends UserRelatedState { | ||||||
|             const feature = { |             const feature = { | ||||||
|                 type: "Feature", |                 type: "Feature", | ||||||
|                 properties: { |                 properties: { | ||||||
|                     "id": "location_track", |                     id: "location_track", | ||||||
|                     "_date:now": new Date().toISOString(), |                     "_date:now": new Date().toISOString(), | ||||||
|                 }, |                 }, | ||||||
|                 geometry: { |                 geometry: { | ||||||
|                     type: "LineString", |                     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) |             self.allElements.ContainingFeatures.set(feature.properties.id, feature) | ||||||
| 
 | 
 | ||||||
|             return [{ |             return [ | ||||||
|                 feature, |                 { | ||||||
|                 freshness: new Date() |                     feature, | ||||||
|             }] |                     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) { |         if (gpsLineLayerDef !== undefined) { | ||||||
|             this.historicalUserLocationsTrack = new TiledStaticFeatureSource(asLine, gpsLineLayerDef); |             this.historicalUserLocationsTrack = new TiledStaticFeatureSource( | ||||||
|  |                 asLine, | ||||||
|  |                 gpsLineLayerDef | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private initHomeLocation() { |     private initHomeLocation() { | ||||||
|         const empty = [] |         const empty = [] | ||||||
|         const feature = Stores.ListStabilized(this.osmConnection.userDetails.map(userDetails => { |         const feature = Stores.ListStabilized( | ||||||
| 
 |             this.osmConnection.userDetails.map((userDetails) => { | ||||||
|             if (userDetails === undefined) { |                 if (userDetails === undefined) { | ||||||
|                 return undefined; |                     return undefined | ||||||
|             } |                 } | ||||||
|             const home = userDetails.home; |                 const home = userDetails.home | ||||||
|             if (home === undefined) { |                 if (home === undefined) { | ||||||
|                 return undefined; |                     return undefined | ||||||
|             } |                 } | ||||||
|             return [home.lon, home.lat] |                 return [home.lon, home.lat] | ||||||
|         })).map(homeLonLat => { |             }) | ||||||
|  |         ).map((homeLonLat) => { | ||||||
|             if (homeLonLat === undefined) { |             if (homeLonLat === undefined) { | ||||||
|                 return empty |                 return empty | ||||||
|             } |             } | ||||||
|             return [{ |             return [ | ||||||
|                 feature: { |                 { | ||||||
|                     "type": "Feature", |                     feature: { | ||||||
|                     "properties": { |                         type: "Feature", | ||||||
|                         "id": "home", |                         properties: { | ||||||
|                         "user:home": "yes", |                             id: "home", | ||||||
|                         "_lon": homeLonLat[0], |                             "user:home": "yes", | ||||||
|                         "_lat": homeLonLat[1] |                             _lon: homeLonLat[0], | ||||||
|  |                             _lat: homeLonLat[1], | ||||||
|  |                         }, | ||||||
|  |                         geometry: { | ||||||
|  |                             type: "Point", | ||||||
|  |                             coordinates: homeLonLat, | ||||||
|  |                         }, | ||||||
|                     }, |                     }, | ||||||
|                     "geometry": { |                     freshness: new Date(), | ||||||
|                         "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) { |         if (flayer !== undefined) { | ||||||
|             this.homeLocation = new TiledStaticFeatureSource(feature, flayer) |             this.homeLocation = new TiledStaticFeatureSource(feature, flayer) | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static getPref(osmConnection: OsmConnection, key: string, layer: LayerConfig): UIEventSource<boolean> { |     private static getPref( | ||||||
|         return osmConnection |         osmConnection: OsmConnection, | ||||||
|             .GetPreference(key, layer.shownByDefault + "") |         key: string, | ||||||
|             .sync(v => { |         layer: LayerConfig | ||||||
|  |     ): UIEventSource<boolean> { | ||||||
|  |         return osmConnection.GetPreference(key, layer.shownByDefault + "").sync( | ||||||
|  |             (v) => { | ||||||
|                 if (v === undefined) { |                 if (v === undefined) { | ||||||
|                     return undefined |                     return undefined | ||||||
|                 } |                 } | ||||||
|                 return v === "true"; |                 return v === "true" | ||||||
|             }, [], b => { |             }, | ||||||
|  |             [], | ||||||
|  |             (b) => { | ||||||
|                 if (b === undefined) { |                 if (b === undefined) { | ||||||
|                     return 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) { |         if (layoutToUse === undefined) { | ||||||
|             return [] |             return [] | ||||||
|         } |         } | ||||||
|         const flayers: FilteredLayer[] = []; |         const flayers: FilteredLayer[] = [] | ||||||
|         for (const layer of layoutToUse.layers) { |         for (const layer of layoutToUse.layers) { | ||||||
|             let isDisplayed: UIEventSource<boolean> |             let isDisplayed: UIEventSource<boolean> | ||||||
|             if (layer.syncSelection === "local") { |             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") { |             } 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") { |             } else if (layer.syncSelection === "global") { | ||||||
|                 isDisplayed = MapState.getPref(osmConnection,"layer-" + layer.id + "-enabled", layer) |                 isDisplayed = MapState.getPref( | ||||||
|  |                     osmConnection, | ||||||
|  |                     "layer-" + layer.id + "-enabled", | ||||||
|  |                     layer | ||||||
|  |                 ) | ||||||
|             } else { |             } 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 = { |             const flayer: FilteredLayer = { | ||||||
|                 isDisplayed, |                 isDisplayed, | ||||||
|                 layerDef: layer, |                 layerDef: layer, | ||||||
|                 appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>()) |                 appliedFilters: new UIEventSource<Map<string, FilterState>>( | ||||||
|             }; |                     new Map<string, FilterState>() | ||||||
|             layer.filters.forEach(filterConfig => { |                 ), | ||||||
|  |             } | ||||||
|  |             layer.filters.forEach((filterConfig) => { | ||||||
|                 const stateSrc = filterConfig.initState() |                 const stateSrc = filterConfig.initState() | ||||||
| 
 | 
 | ||||||
|                 stateSrc.addCallbackAndRun(state => flayer.appliedFilters.data.set(filterConfig.id, state)) |                 stateSrc.addCallbackAndRun((state) => | ||||||
|                 flayer.appliedFilters.map(dict => dict.get(filterConfig.id)) |                     flayer.appliedFilters.data.set(filterConfig.id, state) | ||||||
|                     .addCallback(state => stateSrc.setData(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) { |         for (const layer of layoutToUse.layers) { | ||||||
|             if (layer.filterIsSameAs === undefined) { |             if (layer.filterIsSameAs === undefined) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             const toReuse = flayers.find(l => l.layerDef.id === layer.filterIsSameAs) |             const toReuse = flayers.find((l) => l.layerDef.id === layer.filterIsSameAs) | ||||||
|             if (toReuse === undefined) { |             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) |             console.warn( | ||||||
|             const selfLayer = flayers.findIndex(l => l.layerDef.id === layer.id) |                 "Linking filter and isDisplayed-states of " + | ||||||
|  |                     layer.id + | ||||||
|  |                     " and " + | ||||||
|  |                     layer.filterIsSameAs | ||||||
|  |             ) | ||||||
|  |             const selfLayer = flayers.findIndex((l) => l.layerDef.id === layer.id) | ||||||
|             flayers[selfLayer] = { |             flayers[selfLayer] = { | ||||||
|                 isDisplayed: toReuse.isDisplayed, |                 isDisplayed: toReuse.isDisplayed, | ||||||
|                 layerDef: layer, |                 layerDef: layer, | ||||||
|                 appliedFilters: toReuse.appliedFilters |                 appliedFilters: toReuse.appliedFilters, | ||||||
|             }; |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return flayers; |         return flayers | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,50 +1,48 @@ | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import {OsmConnection} from "../Osm/OsmConnection"; | import { OsmConnection } from "../Osm/OsmConnection" | ||||||
| import {MangroveIdentity} from "../Web/MangroveReviews"; | import { MangroveIdentity } from "../Web/MangroveReviews" | ||||||
| import {Store, UIEventSource} from "../UIEventSource"; | import { Store, UIEventSource } from "../UIEventSource" | ||||||
| import {QueryParameters} from "../Web/QueryParameters"; | import { QueryParameters } from "../Web/QueryParameters" | ||||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import Locale from "../../UI/i18n/Locale"; | import Locale from "../../UI/i18n/Locale" | ||||||
| import ElementsState from "./ElementsState"; | import ElementsState from "./ElementsState" | ||||||
| import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater"; | import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater" | ||||||
| import {Changes} from "../Osm/Changes"; | import { Changes } from "../Osm/Changes" | ||||||
| import ChangeToElementsActor from "../Actors/ChangeToElementsActor"; | import ChangeToElementsActor from "../Actors/ChangeToElementsActor" | ||||||
| import PendingChangesUploader from "../Actors/PendingChangesUploader"; | import PendingChangesUploader from "../Actors/PendingChangesUploader" | ||||||
| import * as translators from "../../assets/translators.json" | 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, |  * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, | ||||||
|  * which layers they enabled, ... |  * which layers they enabled, ... | ||||||
|  */ |  */ | ||||||
| export default class UserRelatedState extends ElementsState { | export default class UserRelatedState extends ElementsState { | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      The user credentials |      The user credentials | ||||||
|      */ |      */ | ||||||
|     public osmConnection: OsmConnection; |     public osmConnection: OsmConnection | ||||||
|     /** |     /** | ||||||
|      THe change handler |      THe change handler | ||||||
|      */ |      */ | ||||||
|     public changes: Changes; |     public changes: Changes | ||||||
|     /** |     /** | ||||||
|      * The key for mangrove |      * The key for mangrove | ||||||
|      */ |      */ | ||||||
|     public mangroveIdentity: MangroveIdentity; |     public mangroveIdentity: MangroveIdentity | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Maproulette connection |      * Maproulette connection | ||||||
|      */ |      */ | ||||||
|     public maprouletteConnection: Maproulette; |     public maprouletteConnection: Maproulette | ||||||
|  | 
 | ||||||
|  |     public readonly isTranslator: Store<boolean> | ||||||
| 
 | 
 | ||||||
|     public readonly isTranslator : Store<boolean>; |  | ||||||
|      |  | ||||||
|     public readonly installedUserThemes: Store<string[]> |     public readonly installedUserThemes: Store<string[]> | ||||||
|      | 
 | ||||||
|     constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { |     constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { | ||||||
|         super(layoutToUse); |         super(layoutToUse) | ||||||
| 
 | 
 | ||||||
|         this.osmConnection = new OsmConnection({ |         this.osmConnection = new OsmConnection({ | ||||||
|             dryRun: this.featureSwitchIsTesting, |             dryRun: this.featureSwitchIsTesting, | ||||||
|  | @ -54,138 +52,147 @@ export default class UserRelatedState extends ElementsState { | ||||||
|                 undefined, |                 undefined, | ||||||
|                 "Used to complete the login" |                 "Used to complete the login" | ||||||
|             ), |             ), | ||||||
|             osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data, |             osmConfiguration: <"osm" | "osm-test">this.featureSwitchApiURL.data, | ||||||
|             attemptLogin: options?.attemptLogin |             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) |         translationMode.syncWith(Locale.showLinkToWeblate) | ||||||
|          | 
 | ||||||
|         this.isTranslator = this.osmConnection.userDetails.map(ud => { |         this.isTranslator = this.osmConnection.userDetails.map((ud) => { | ||||||
|             if(!ud.loggedIn){ |             if (!ud.loggedIn) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|             const name= ud.name.toLowerCase().replace(/\s+/g, '') |             const name = ud.name.toLowerCase().replace(/\s+/g, "") | ||||||
|             return translators.contributors.some(c => c.contributor.toLowerCase().replace(/\s+/g, '') === name) |             return translators.contributors.some( | ||||||
|  |                 (c) => c.contributor.toLowerCase().replace(/\s+/g, "") === name | ||||||
|  |             ) | ||||||
|         }) |         }) | ||||||
|          | 
 | ||||||
|         this.isTranslator.addCallbackAndRunD(ud => { |         this.isTranslator.addCallbackAndRunD((ud) => { | ||||||
|             if(ud){ |             if (ud) { | ||||||
|                 Locale.showLinkToWeblate.setData(true) |                 Locale.showLinkToWeblate.setData(true) | ||||||
|             } |             } | ||||||
|         }); |         }) | ||||||
|          | 
 | ||||||
|         this.changes = new Changes(this, layoutToUse?.isLeftRightSensitive() ?? false) |         this.changes = new Changes(this, layoutToUse?.isLeftRightSensitive() ?? false) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         new ChangeToElementsActor(this.changes, this.allElements) |         new ChangeToElementsActor(this.changes, this.allElements) | ||||||
|         new PendingChangesUploader(this.changes, this.selectedElement); |         new PendingChangesUploader(this.changes, this.selectedElement) | ||||||
|          | 
 | ||||||
|         this.mangroveIdentity = new MangroveIdentity( |         this.mangroveIdentity = new MangroveIdentity( | ||||||
|             this.osmConnection.GetLongPreference("identity", "mangrove") |             this.osmConnection.GetLongPreference("identity", "mangrove") | ||||||
|         ); |         ) | ||||||
| 
 | 
 | ||||||
|         this.maprouletteConnection = new Maproulette(); |         this.maprouletteConnection = new Maproulette() | ||||||
| 
 | 
 | ||||||
|         if (layoutToUse?.hideFromOverview) { |         if (layoutToUse?.hideFromOverview) { | ||||||
|             this.osmConnection.isLoggedIn.addCallbackAndRunD(loggedIn => { |             this.osmConnection.isLoggedIn.addCallbackAndRunD((loggedIn) => { | ||||||
|                 if (loggedIn) { |                 if (loggedIn) { | ||||||
|                     this.osmConnection |                     this.osmConnection | ||||||
|                         .GetPreference("hidden-theme-" + layoutToUse?.id + "-enabled") |                         .GetPreference("hidden-theme-" + layoutToUse?.id + "-enabled") | ||||||
|                         .setData("true"); |                         .setData("true") | ||||||
|                     return true; |                     return true | ||||||
|                 } |                 } | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (this.layoutToUse !== undefined && !this.layoutToUse.official) { |         if (this.layoutToUse !== undefined && !this.layoutToUse.official) { | ||||||
|             console.log("Marking unofficial theme as visited") |             console.log("Marking unofficial theme as visited") | ||||||
|             this.osmConnection.GetLongPreference("unofficial-theme-" + this.layoutToUse.id) |             this.osmConnection.GetLongPreference("unofficial-theme-" + this.layoutToUse.id).setData( | ||||||
|                 .setData(JSON.stringify({ |                 JSON.stringify({ | ||||||
|                     id: this.layoutToUse.id, |                     id: this.layoutToUse.id, | ||||||
|                     icon: this.layoutToUse.icon, |                     icon: this.layoutToUse.icon, | ||||||
|                     title: this.layoutToUse.title.translations, |                     title: this.layoutToUse.title.translations, | ||||||
|                     shortDescription: this.layoutToUse.shortDescription.translations, |                     shortDescription: this.layoutToUse.shortDescription.translations, | ||||||
|                     definition: this.layoutToUse["definition"] |                     definition: this.layoutToUse["definition"], | ||||||
|                 })) |                 }) | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.InitializeLanguage(); |         this.InitializeLanguage() | ||||||
|         new SelectedElementTagsUpdater(this) |         new SelectedElementTagsUpdater(this) | ||||||
|         this.installedUserThemes = this.InitInstalledUserThemes(); |         this.installedUserThemes = this.InitInstalledUserThemes() | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private InitializeLanguage() { |     private InitializeLanguage() { | ||||||
|         const layoutToUse = this.layoutToUse; |         const layoutToUse = this.layoutToUse | ||||||
|         Locale.language.syncWith(this.osmConnection.GetPreference("language")); |         Locale.language.syncWith(this.osmConnection.GetPreference("language")) | ||||||
|         Locale.language |         Locale.language.addCallback((currentLanguage) => { | ||||||
|             .addCallback((currentLanguage) => { |             if (layoutToUse === undefined) { | ||||||
|                 if (layoutToUse === undefined) { |                 return | ||||||
|                     return; |             } | ||||||
|                 } |             if (Locale.showLinkToWeblate.data) { | ||||||
|                 if(Locale.showLinkToWeblate.data){ |                 return true // Disable auto switching as we are in translators mode
 | ||||||
|                     return true; // Disable auto switching as we are in translators mode
 |             } | ||||||
|                 } |             if (this.layoutToUse.language.indexOf(currentLanguage) < 0) { | ||||||
|                 if (this.layoutToUse.language.indexOf(currentLanguage) < 0) { |                 console.log( | ||||||
|                     console.log( |                     "Resetting language to", | ||||||
|                         "Resetting language to", |                     layoutToUse.language[0], | ||||||
|                         layoutToUse.language[0], |                     "as", | ||||||
|                         "as", |                     currentLanguage, | ||||||
|                         currentLanguage, |                     " is unsupported" | ||||||
|                         " is unsupported" |                 ) | ||||||
|                     ); |                 // The current language is not supported -> switch to a supported one
 | ||||||
|                     // 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[]>{ |     private InitInstalledUserThemes(): Store<string[]> { | ||||||
|         const prefix = "mapcomplete-unofficial-theme-"; |         const prefix = "mapcomplete-unofficial-theme-" | ||||||
|         const postfix = "-combined-length" |         const postfix = "-combined-length" | ||||||
|         return this.osmConnection.preferencesHandler.preferences.map(prefs => |         return this.osmConnection.preferencesHandler.preferences.map((prefs) => | ||||||
|             Object.keys(prefs) |             Object.keys(prefs) | ||||||
|                 .filter(k => k.startsWith(prefix) && k.endsWith(postfix)) |                 .filter((k) => k.startsWith(prefix) && k.endsWith(postfix)) | ||||||
|                 .map(k => k.substring(prefix.length, k.length - postfix.length)) |                 .map((k) => k.substring(prefix.length, k.length - postfix.length)) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     public GetUnofficialTheme(id: string):  { |     public GetUnofficialTheme(id: string): | ||||||
|         id: string |         | { | ||||||
|         icon: string, |               id: string | ||||||
|         title: any, |               icon: string | ||||||
|         shortDescription: any, |               title: any | ||||||
|         definition?: any, |               shortDescription: any | ||||||
|         isOfficial: boolean |               definition?: any | ||||||
|     } | undefined { |               isOfficial: boolean | ||||||
|  |           } | ||||||
|  |         | undefined { | ||||||
|         console.log("GETTING UNOFFICIAL THEME") |         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 |         const str = pref.data | ||||||
|          | 
 | ||||||
|         if (str === undefined || str === "undefined" || str === "") { |         if (str === undefined || str === "undefined" || str === "") { | ||||||
|             pref.setData(null) |             pref.setData(null) | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         try { |         try { | ||||||
|             const value: { |             const value: { | ||||||
|                 id: string |                 id: string | ||||||
|                 icon: string, |                 icon: string | ||||||
|                 title: any, |                 title: any | ||||||
|                 shortDescription: any, |                 shortDescription: any | ||||||
|                 definition?: any, |                 definition?: any | ||||||
|                 isOfficial: boolean |                 isOfficial: boolean | ||||||
|             } = JSON.parse(str) |             } = JSON.parse(str) | ||||||
|             value.isOfficial = false |             value.isOfficial = false | ||||||
|             return value; |             return value | ||||||
|         } catch (e) { |         } 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) |             pref.setData(null) | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|          |  | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,15 +1,14 @@ | ||||||
| import {TagsFilter} from "./TagsFilter"; | import { TagsFilter } from "./TagsFilter" | ||||||
| import {Or} from "./Or"; | import { Or } from "./Or" | ||||||
| import {TagUtils} from "./TagUtils"; | import { TagUtils } from "./TagUtils" | ||||||
| import {Tag} from "./Tag"; | import { Tag } from "./Tag" | ||||||
| import {RegexTag} from "./RegexTag"; | import { RegexTag } from "./RegexTag" | ||||||
| 
 | 
 | ||||||
| export class And extends TagsFilter { | export class And extends TagsFilter { | ||||||
| 
 |  | ||||||
|     public and: TagsFilter[] |     public and: TagsFilter[] | ||||||
| 
 | 
 | ||||||
|     constructor(and: TagsFilter[]) { |     constructor(and: TagsFilter[]) { | ||||||
|         super(); |         super() | ||||||
|         this.and = and |         this.and = and | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -21,11 +20,11 @@ export class And extends TagsFilter { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static combine(filter: string, choices: string[]): string[] { |     private static combine(filter: string, choices: string[]): string[] { | ||||||
|         const values = []; |         const values = [] | ||||||
|         for (const or of choices) { |         for (const or of choices) { | ||||||
|             values.push(filter + or); |             values.push(filter + or) | ||||||
|         } |         } | ||||||
|         return values; |         return values | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     normalize() { |     normalize() { | ||||||
|  | @ -43,11 +42,11 @@ export class And extends TagsFilter { | ||||||
|     matchesProperties(tags: any): boolean { |     matchesProperties(tags: any): boolean { | ||||||
|         for (const tagsFilter of this.and) { |         for (const tagsFilter of this.and) { | ||||||
|             if (!tagsFilter.matchesProperties(tags)) { |             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\"]" ]
 |      * and.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]" ]
 | ||||||
|      */ |      */ | ||||||
|     asOverpass(): string[] { |     asOverpass(): string[] { | ||||||
|         let allChoices: string[] = null; |         let allChoices: string[] = null | ||||||
|         for (const andElement of this.and) { |         for (const andElement of this.and) { | ||||||
|             const andElementFilter = andElement.asOverpass(); |             const andElementFilter = andElement.asOverpass() | ||||||
|             if (allChoices === null) { |             if (allChoices === null) { | ||||||
|                 allChoices = andElementFilter; |                 allChoices = andElementFilter | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const newChoices: string[] = []; |             const newChoices: string[] = [] | ||||||
|             for (const choice of allChoices) { |             for (const choice of allChoices) { | ||||||
|                 newChoices.push( |                 newChoices.push(...And.combine(choice, andElementFilter)) | ||||||
|                     ...And.combine(choice, andElementFilter) |  | ||||||
|                 ) |  | ||||||
|             } |             } | ||||||
|             allChoices = newChoices; |             allChoices = newChoices | ||||||
|         } |         } | ||||||
|         return allChoices; |         return allChoices | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asHumanString(linkToWiki: boolean, shorten: boolean, properties) { |     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 { |     isUsableAsAnswer(): boolean { | ||||||
|         for (const t of this.and) { |         for (const t of this.and) { | ||||||
|             if (!t.isUsableAsAnswer()) { |             if (!t.isUsableAsAnswer()) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return true; |         return true | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -107,45 +107,44 @@ export class And extends TagsFilter { | ||||||
|      */ |      */ | ||||||
|     shadows(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         if (!(other instanceof And)) { |         if (!(other instanceof And)) { | ||||||
|             return false; |             return false | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         for (const selfTag of this.and) { |         for (const selfTag of this.and) { | ||||||
|             let matchFound = false; |             let matchFound = false | ||||||
|             for (const otherTag of other.and) { |             for (const otherTag of other.and) { | ||||||
|                 matchFound = selfTag.shadows(otherTag); |                 matchFound = selfTag.shadows(otherTag) | ||||||
|                 if (matchFound) { |                 if (matchFound) { | ||||||
|                     break; |                     break | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             if (!matchFound) { |             if (!matchFound) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         for (const otherTag of other.and) { |         for (const otherTag of other.and) { | ||||||
|             let matchFound = false; |             let matchFound = false | ||||||
|             for (const selfTag of this.and) { |             for (const selfTag of this.and) { | ||||||
|                 matchFound = selfTag.shadows(otherTag); |                 matchFound = selfTag.shadows(otherTag) | ||||||
|                 if (matchFound) { |                 if (matchFound) { | ||||||
|                     break; |                     break | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             if (!matchFound) { |             if (!matchFound) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |         return true | ||||||
|         return true; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     usedKeys(): string[] { |     usedKeys(): string[] { | ||||||
|         return [].concat(...this.and.map(subkeys => subkeys.usedKeys())); |         return [].concat(...this.and.map((subkeys) => subkeys.usedKeys())) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     usedTags(): { key: string; value: string }[] { |     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 }[] { |     asChange(properties: any): { k: string; v: string }[] { | ||||||
|  | @ -153,7 +152,7 @@ export class And extends TagsFilter { | ||||||
|         for (const tagsFilter of this.and) { |         for (const tagsFilter of this.and) { | ||||||
|             result.push(...tagsFilter.asChange(properties)) |             result.push(...tagsFilter.asChange(properties)) | ||||||
|         } |         } | ||||||
|         return result; |         return result | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -187,7 +186,7 @@ export class And extends TagsFilter { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|                 if (r === false) { |                 if (r === false) { | ||||||
|                     return false; |                     return false | ||||||
|                 } |                 } | ||||||
|                 newAnds.push(r) |                 newAnds.push(r) | ||||||
|                 continue |                 continue | ||||||
|  | @ -203,7 +202,6 @@ export class And extends TagsFilter { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             if (!value && tag.shadows(knownExpression)) { |             if (!value && tag.shadows(knownExpression)) { | ||||||
| 
 |  | ||||||
|                 /** |                 /** | ||||||
|                  * We know that knownExpression is unmet. |                  * We know that knownExpression is unmet. | ||||||
|                  * if the tag shadows 'knownExpression' (which is the case when control flows gets here), |                  * if the tag shadows 'knownExpression' (which is the case when control flows gets here), | ||||||
|  | @ -228,49 +226,50 @@ export class And extends TagsFilter { | ||||||
|         if (this.and.length === 0) { |         if (this.and.length === 0) { | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|         const optimizedRaw = this.and.map(t => t.optimize()) |         const optimizedRaw = this.and | ||||||
|             .filter(t => t !== true /* true is the neutral element in an AND, we drop them*/) |             .map((t) => t.optimize()) | ||||||
|         if (optimizedRaw.some(t => t === false)) { |             .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'
 |             // 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
 |             // Conflicting keys do return false
 | ||||||
|             const properties: object =  {} |             const properties: object = {} | ||||||
|             for (const opt of optimized) { |             for (const opt of optimized) { | ||||||
|                 if (opt instanceof Tag) { |                 if (opt instanceof Tag) { | ||||||
|                     properties[opt.key] = opt.value |                     properties[opt.key] = opt.value | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|              for (const opt of optimized) { |             for (const opt of optimized) { | ||||||
|                  if(opt instanceof Tag ){ |                 if (opt instanceof Tag) { | ||||||
|                      const k = opt.key |                     const k = opt.key | ||||||
|                      const v = properties[k] |                     const v = properties[k] | ||||||
|                      if(v === undefined){ |                     if (v === undefined) { | ||||||
|                          continue |                         continue | ||||||
|                      } |                     } | ||||||
|                      if(v !== opt.value){ |                     if (v !== opt.value) { | ||||||
|                          // detected an internal conflict 
 |                         // detected an internal conflict
 | ||||||
|                          return false |                         return false | ||||||
|                      } |                     } | ||||||
|                  } |                 } | ||||||
|                  if(opt instanceof RegexTag ){ |                 if (opt instanceof RegexTag) { | ||||||
|                      const k = opt.key |                     const k = opt.key | ||||||
|                      if(typeof k !== "string"){ |                     if (typeof k !== "string") { | ||||||
|                          continue |                         continue | ||||||
|                      } |                     } | ||||||
|                      const v = properties[k] |                     const v = properties[k] | ||||||
|                      if(v === undefined){ |                     if (v === undefined) { | ||||||
|                          continue |                         continue | ||||||
|                      } |                     } | ||||||
|                      if(v !== opt.value){ |                     if (v !== opt.value) { | ||||||
|                          // detected an internal conflict 
 |                         // detected an internal conflict
 | ||||||
|                          return false |                         return false | ||||||
|                      } |                     } | ||||||
|                  } |                 } | ||||||
|              } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const newAnds: TagsFilter[] = [] |         const newAnds: TagsFilter[] = [] | ||||||
|  | @ -287,7 +286,7 @@ export class And extends TagsFilter { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         { |         { | ||||||
|             let dirty = false; |             let dirty = false | ||||||
|             do { |             do { | ||||||
|                 const cleanedContainedOrs: Or[] = [] |                 const cleanedContainedOrs: Or[] = [] | ||||||
|                 outer: for (let containedOr of containedOrs) { |                 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
 |                         // the 'or' dissolved into a normal tag -> it has to be added to the newAnds
 | ||||||
|                         newAnds.push(cleaned) |                         newAnds.push(cleaned) | ||||||
|                         dirty = true; // rerun this algo later on
 |                         dirty = true // rerun this algo later on
 | ||||||
|                         continue outer; |                         continue outer | ||||||
|                     } |                     } | ||||||
|                     cleanedContainedOrs.push(containedOr) |                     cleanedContainedOrs.push(containedOr) | ||||||
|                 } |                 } | ||||||
|  | @ -319,30 +318,32 @@ export class And extends TagsFilter { | ||||||
|             } while (dirty) |             } while (dirty) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |         containedOrs = containedOrs.filter((ca) => { | ||||||
|         containedOrs = containedOrs.filter(ca => { |  | ||||||
|             const isShadowed = TagUtils.containsEquivalents(newAnds, ca.or) |             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
 |             // 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
 |             // XY & (XY | AB) === XY
 | ||||||
|             return !isShadowed; |             return !isShadowed | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         // Extract common keys from the OR
 |         // Extract common keys from the OR
 | ||||||
|         if (containedOrs.length === 1) { |         if (containedOrs.length === 1) { | ||||||
|             newAnds.push(containedOrs[0]) |             newAnds.push(containedOrs[0]) | ||||||
|         } else if (containedOrs.length > 1) { |         } 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++) { |             for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++) { | ||||||
|                 const containedOr = containedOrs[i]; |                 const containedOr = containedOrs[i] | ||||||
|                 commonValues = commonValues.filter(cv => containedOr.or.some(candidate => candidate.shadows(cv))) |                 commonValues = commonValues.filter((cv) => | ||||||
|  |                     containedOr.or.some((candidate) => candidate.shadows(cv)) | ||||||
|  |                 ) | ||||||
|             } |             } | ||||||
|             if (commonValues.length === 0) { |             if (commonValues.length === 0) { | ||||||
|                 newAnds.push(...containedOrs) |                 newAnds.push(...containedOrs) | ||||||
|             } else { |             } else { | ||||||
|                 const newOrs: TagsFilter[] = [] |                 const newOrs: TagsFilter[] = [] | ||||||
|                 for (const containedOr of containedOrs) { |                 for (const containedOr of containedOrs) { | ||||||
|                     const elements = containedOr.or |                     const elements = containedOr.or.filter( | ||||||
|                         .filter(candidate => !commonValues.some(cv => cv.shadows(candidate))) |                         (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) | ||||||
|  |                     ) | ||||||
|                     newOrs.push(Or.construct(elements)) |                     newOrs.push(Or.construct(elements)) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|  | @ -371,12 +372,11 @@ export class And extends TagsFilter { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|         return !this.and.some(t => !t.isNegative()); |         return !this.and.some((t) => !t.isNegative()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     visit(f: (TagsFilter: any) => void) { |     visit(f: (TagsFilter: any) => void) { | ||||||
|         f(this) |         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 { | export default class ComparingTag implements TagsFilter { | ||||||
|     private readonly _key: string; |     private readonly _key: string | ||||||
|     private readonly _predicate: (value: string) => boolean; |     private readonly _predicate: (value: string) => boolean | ||||||
|     private readonly _representation: string; |     private readonly _representation: string | ||||||
| 
 | 
 | ||||||
|     constructor(key: string, predicate: (value: string | undefined) => boolean, representation: string = "") { |     constructor( | ||||||
|         this._key = key; |         key: string, | ||||||
|         this._predicate = predicate; |         predicate: (value: string | undefined) => boolean, | ||||||
|         this._representation = representation; |         representation: string = "" | ||||||
|  |     ) { | ||||||
|  |         this._key = key | ||||||
|  |         this._predicate = predicate | ||||||
|  |         this._representation = representation | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asChange(properties: any): { k: string; v: string }[] { |     asChange(properties: any): { k: string; v: string }[] { | ||||||
|  | @ -24,16 +28,16 @@ export default class ComparingTag implements TagsFilter { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     shadows(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         return other === this; |         return other === this | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isUsableAsAnswer(): boolean { |     isUsableAsAnswer(): boolean { | ||||||
|         return false; |         return false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Checks if the properties match |      * Checks if the properties match | ||||||
|      *  |      * | ||||||
|      * const t = new ComparingTag("key", (x => Number(x) < 42)) |      * const t = new ComparingTag("key", (x => Number(x) < 42)) | ||||||
|      * t.matchesProperties({key: 42}) // => false
 |      * t.matchesProperties({key: 42}) // => false
 | ||||||
|      * t.matchesProperties({key: 41}) // => true
 |      * t.matchesProperties({key: 41}) // => true
 | ||||||
|  | @ -41,26 +45,26 @@ export default class ComparingTag implements TagsFilter { | ||||||
|      * t.matchesProperties({differentKey: 42}) // => false
 |      * t.matchesProperties({differentKey: 42}) // => false
 | ||||||
|      */ |      */ | ||||||
|     matchesProperties(properties: any): boolean { |     matchesProperties(properties: any): boolean { | ||||||
|         return this._predicate(properties[this._key]); |         return this._predicate(properties[this._key]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     usedKeys(): string[] { |     usedKeys(): string[] { | ||||||
|         return [this._key]; |         return [this._key] | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     usedTags(): { key: string; value: string }[] { |     usedTags(): { key: string; value: string }[] { | ||||||
|         return []; |         return [] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): TagsFilter | boolean { | ||||||
|         return this; |         return this | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|         return true; |         return true | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     visit(f: (TagsFilter) => void) { |     visit(f: (TagsFilter) => void) { | ||||||
|         f(this) |         f(this) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										168
									
								
								Logic/Tags/Or.ts
									
										
									
									
									
								
							
							
						
						
									
										168
									
								
								Logic/Tags/Or.ts
									
										
									
									
									
								
							|  | @ -1,88 +1,85 @@ | ||||||
| import {TagsFilter} from "./TagsFilter"; | import { TagsFilter } from "./TagsFilter" | ||||||
| import {TagUtils} from "./TagUtils"; | import { TagUtils } from "./TagUtils" | ||||||
| import {And} from "./And"; | import { And } from "./And" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| export class Or extends TagsFilter { | export class Or extends TagsFilter { | ||||||
|     public or: TagsFilter[] |     public or: TagsFilter[] | ||||||
| 
 | 
 | ||||||
|     constructor(or: TagsFilter[]) { |     constructor(or: TagsFilter[]) { | ||||||
|         super(); |         super() | ||||||
|         this.or = or; |         this.or = or | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static construct(or: TagsFilter[]): TagsFilter{ |     public static construct(or: TagsFilter[]): TagsFilter { | ||||||
|         if(or.length === 1){ |         if (or.length === 1) { | ||||||
|             return or[0] |             return or[0] | ||||||
|         } |         } | ||||||
|         return new Or(or) |         return new Or(or) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     matchesProperties(properties: any): boolean { |     matchesProperties(properties: any): boolean { | ||||||
|         for (const tagsFilter of this.or) { |         for (const tagsFilter of this.or) { | ||||||
|             if (tagsFilter.matchesProperties(properties)) { |             if (tagsFilter.matchesProperties(properties)) { | ||||||
|                 return true; |                 return true | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return false; |         return false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * |      * | ||||||
|      * import {Tag} from "./Tag"; |      * import {Tag} from "./Tag"; | ||||||
|      * import {RegexTag} from "./RegexTag"; |      * import {RegexTag} from "./RegexTag"; | ||||||
|      *  |      * | ||||||
|      * const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)]) |      * const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)]) | ||||||
|      * const or = new Or([and, new Tag("leisure", "nature_reserve"]) |      * const or = new Or([and, new Tag("leisure", "nature_reserve"]) | ||||||
|      * or.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]", "[\"leisure\"=\"nature_reserve\"]" ]
 |      * or.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]", "[\"leisure\"=\"nature_reserve\"]" ]
 | ||||||
|      *  |      * | ||||||
|      * // should fuse nested ors into a single list
 |      * // should fuse nested ors into a single list
 | ||||||
|      * const or = new Or([new Tag("key","value"), new Or([new Tag("key1","value1"), new Tag("key2","value2")])]) |      * const or = new Or([new Tag("key","value"), new Or([new Tag("key1","value1"), new Tag("key2","value2")])]) | ||||||
|      * or.asOverpass() // => [ `["key"="value"]`, `["key1"="value1"]`, `["key2"="value2"]` ]
 |      * or.asOverpass() // => [ `["key"="value"]`, `["key1"="value1"]`, `["key2"="value2"]` ]
 | ||||||
|      */ |      */ | ||||||
|     asOverpass(): string[] { |     asOverpass(): string[] { | ||||||
|         const choices = []; |         const choices = [] | ||||||
|         for (const tagsFilter of this.or) { |         for (const tagsFilter of this.or) { | ||||||
|             const subChoices = tagsFilter.asOverpass(); |             const subChoices = tagsFilter.asOverpass() | ||||||
|             choices.push(...subChoices) |             choices.push(...subChoices) | ||||||
|         } |         } | ||||||
|         return choices; |         return choices | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asHumanString(linkToWiki: boolean, shorten: boolean, properties) { |     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 { |     isUsableAsAnswer(): boolean { | ||||||
|         return false; |         return false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     shadows(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         if (other instanceof Or) { |         if (other instanceof Or) { | ||||||
| 
 |  | ||||||
|             for (const selfTag of this.or) { |             for (const selfTag of this.or) { | ||||||
|                 let matchFound = false; |                 let matchFound = false | ||||||
|                 for (let i = 0; i < other.or.length && !matchFound; i++) { |                 for (let i = 0; i < other.or.length && !matchFound; i++) { | ||||||
|                     let otherTag = other.or[i]; |                     let otherTag = other.or[i] | ||||||
|                     matchFound = selfTag.shadows(otherTag); |                     matchFound = selfTag.shadows(otherTag) | ||||||
|                 } |                 } | ||||||
|                 if (!matchFound) { |                 if (!matchFound) { | ||||||
|                     return false; |                     return false | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             return true; |             return true | ||||||
|         } |         } | ||||||
|         return false; |         return false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     usedKeys(): string[] { |     usedKeys(): string[] { | ||||||
|         return [].concat(...this.or.map(subkeys => subkeys.usedKeys())); |         return [].concat(...this.or.map((subkeys) => subkeys.usedKeys())) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     usedTags(): { key: string; value: string }[] { |     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 }[] { |     asChange(properties: any): { k: string; v: string }[] { | ||||||
|  | @ -90,7 +87,7 @@ export class Or extends TagsFilter { | ||||||
|         for (const tagsFilter of this.or) { |         for (const tagsFilter of this.or) { | ||||||
|             result.push(...tagsFilter.asChange(properties)) |             result.push(...tagsFilter.asChange(properties)) | ||||||
|         } |         } | ||||||
|         return result; |         return result | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -99,7 +96,7 @@ export class Or extends TagsFilter { | ||||||
|      *        ^---------^ |      *        ^---------^ | ||||||
|      * When the evaluation hits (A=B | X=Y), we know _for sure_ that X=Y _does match, as it would have failed the first clause otherwise. |      * When the evaluation hits (A=B | X=Y), we know _for sure_ that X=Y _does match, as it would have failed the first clause otherwise. | ||||||
|      * This means we can safely ignore this in the OR |      * This means we can safely ignore this in the OR | ||||||
|      *  |      * | ||||||
|      * new Or([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // =>true
 |      * new Or([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // =>true
 | ||||||
|      * new Or([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), false) // => new Tag("other_key","value")
 |      * new Or([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), false) // => new Tag("other_key","value")
 | ||||||
|      * new Or([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), true) // => true
 |      * new Or([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), true) // => true
 | ||||||
|  | @ -109,21 +106,21 @@ export class Or extends TagsFilter { | ||||||
|     removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { |     removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { | ||||||
|         const newOrs: TagsFilter[] = [] |         const newOrs: TagsFilter[] = [] | ||||||
|         for (const tag of this.or) { |         for (const tag of this.or) { | ||||||
|             if(tag instanceof Or){ |             if (tag instanceof Or) { | ||||||
|                 throw "Optimize expressions before using removePhraseConsideredKnown" |                 throw "Optimize expressions before using removePhraseConsideredKnown" | ||||||
|             } |             } | ||||||
|             if(tag instanceof And){ |             if (tag instanceof And) { | ||||||
|                 const r = tag.removePhraseConsideredKnown(knownExpression, value) |                 const r = tag.removePhraseConsideredKnown(knownExpression, value) | ||||||
|                 if(r === false){ |                 if (r === false) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|                 if(r === true){ |                 if (r === true) { | ||||||
|                     return true; |                     return true | ||||||
|                 } |                 } | ||||||
|                 newOrs.push(r) |                 newOrs.push(r) | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             if(value && knownExpression.shadows(tag)){ |             if (value && knownExpression.shadows(tag)) { | ||||||
|                 /** |                 /** | ||||||
|                  * At this point, we do know that 'knownExpression' is true in every case |                  * 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, |                  * 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 |                  * "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. |                  * We know that knownExpression is unmet. | ||||||
|                  * if the tag shadows 'knownExpression' (which is the case when control flows gets here), |                  * if the tag shadows 'knownExpression' (which is the case when control flows gets here), | ||||||
|  | @ -143,49 +139,48 @@ export class Or extends TagsFilter { | ||||||
|                  * This implies that 'tag' must be false too! |                  * This implies that 'tag' must be false too! | ||||||
|                  * false is the neutral element in an OR |                  * false is the neutral element in an OR | ||||||
|                  */ |                  */ | ||||||
|                continue |                 continue | ||||||
|             } |             } | ||||||
|             newOrs.push(tag) |             newOrs.push(tag) | ||||||
|         } |         } | ||||||
|         if(newOrs.length === 0){ |         if (newOrs.length === 0) { | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
|         return Or.construct(newOrs) |         return Or.construct(newOrs) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     optimize(): TagsFilter | boolean { |  | ||||||
|          |  | ||||||
|         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)){ |  | ||||||
|             // We have an OR with a contained true: this is always 'true'
 |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|         const optimized = <TagsFilter[]> optimizedRaw; |  | ||||||
|          |  | ||||||
| 
 | 
 | ||||||
|         const newOrs : TagsFilter[] = [] |     optimize(): TagsFilter | boolean { | ||||||
|         let containedAnds : And[] = [] |         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)) { | ||||||
|  |             // We have an OR with a contained true: this is always 'true'
 | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |         const optimized = <TagsFilter[]>optimizedRaw | ||||||
|  | 
 | ||||||
|  |         const newOrs: TagsFilter[] = [] | ||||||
|  |         let containedAnds: And[] = [] | ||||||
|         for (const tf of optimized) { |         for (const tf of optimized) { | ||||||
|             if(tf instanceof Or){ |             if (tf instanceof Or) { | ||||||
|                 // expand all the nested ors...
 |                 // expand all the nested ors...
 | ||||||
|                 newOrs.push(...tf.or) |                 newOrs.push(...tf.or) | ||||||
|             }else if(tf instanceof And){ |             } else if (tf instanceof And) { | ||||||
|                 // partition of all the ands
 |                 // partition of all the ands
 | ||||||
|                 containedAnds.push(tf) |                 containedAnds.push(tf) | ||||||
|             } else { |             } else { | ||||||
|                 newOrs.push(tf) |                 newOrs.push(tf) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         { |         { | ||||||
|             let dirty = false; |             let dirty = false | ||||||
|             do { |             do { | ||||||
|                 const cleanedContainedANds : And[] = [] |                 const cleanedContainedANds: And[] = [] | ||||||
|                 outer: for (let containedAnd of containedAnds) { |                 outer: for (let containedAnd of containedAnds) { | ||||||
|                     for (const known of newOrs) { |                     for (const known of newOrs) { | ||||||
|                         // input for optimazation: (K=V | (X=Y & K=V))
 |                         // input for optimazation: (K=V | (X=Y & K=V))
 | ||||||
|  | @ -206,66 +201,67 @@ export class Or extends TagsFilter { | ||||||
|                         } |                         } | ||||||
|                         // the 'and' dissolved into a normal tag -> it has to be added to the newOrs
 |                         // the 'and' dissolved into a normal tag -> it has to be added to the newOrs
 | ||||||
|                         newOrs.push(cleaned) |                         newOrs.push(cleaned) | ||||||
|                         dirty = true; // rerun this algo later on
 |                         dirty = true // rerun this algo later on
 | ||||||
|                         continue outer; |                         continue outer | ||||||
|                     } |                     } | ||||||
|                     cleanedContainedANds.push(containedAnd) |                     cleanedContainedANds.push(containedAnd) | ||||||
|                 } |                 } | ||||||
|                 containedAnds = cleanedContainedANds |                 containedAnds = cleanedContainedANds | ||||||
|             } while(dirty) |             } while (dirty) | ||||||
|         } |         } | ||||||
|         // Extract common keys from the ANDS
 |         // Extract common keys from the ANDS
 | ||||||
|         if(containedAnds.length === 1){ |         if (containedAnds.length === 1) { | ||||||
|             newOrs.push(containedAnds[0]) |             newOrs.push(containedAnds[0]) | ||||||
|         } else if(containedAnds.length > 1){ |         } else if (containedAnds.length > 1) { | ||||||
|             let commonValues : TagsFilter [] = containedAnds[0].and |             let commonValues: TagsFilter[] = containedAnds[0].and | ||||||
|             for (let i = 1; i < containedAnds.length && commonValues.length > 0; i++){ |             for (let i = 1; i < containedAnds.length && commonValues.length > 0; i++) { | ||||||
|                 const containedAnd = containedAnds[i]; |                 const containedAnd = containedAnds[i] | ||||||
|                 commonValues = commonValues.filter(cv => containedAnd.and.some(candidate => candidate.shadows(cv))) |                 commonValues = commonValues.filter((cv) => | ||||||
|  |                     containedAnd.and.some((candidate) => candidate.shadows(cv)) | ||||||
|  |                 ) | ||||||
|             } |             } | ||||||
|             if(commonValues.length === 0){ |             if (commonValues.length === 0) { | ||||||
|                 newOrs.push(...containedAnds) |                 newOrs.push(...containedAnds) | ||||||
|             }else{ |             } else { | ||||||
|                 const newAnds: TagsFilter[] = [] |                 const newAnds: TagsFilter[] = [] | ||||||
|                 for (const containedAnd of containedAnds) { |                 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)) |                     newAnds.push(And.construct(elements)) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 commonValues.push(Or.construct(newAnds)) |                 commonValues.push(Or.construct(newAnds)) | ||||||
|                 const result = new And(commonValues).optimize() |                 const result = new And(commonValues).optimize() | ||||||
|                 if(result === true){ |                 if (result === true) { | ||||||
|                     return true |                     return true | ||||||
|                 }else if(result === false){ |                 } else if (result === false) { | ||||||
|                     // neutral element: skip
 |                     // neutral element: skip
 | ||||||
|                 }else{ |                 } else { | ||||||
|                     newOrs.push(And.construct(commonValues)) |                     newOrs.push(And.construct(commonValues)) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if(newOrs.length === 0){ |         if (newOrs.length === 0) { | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if(TagUtils.ContainsOppositeTags(newOrs)){ |         if (TagUtils.ContainsOppositeTags(newOrs)) { | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         TagUtils.sortFilters(newOrs, false) |         TagUtils.sortFilters(newOrs, false) | ||||||
| 
 | 
 | ||||||
|         return Or.construct(newOrs) |         return Or.construct(newOrs) | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|         return this.or.some(t => t.isNegative()); |         return this.or.some((t) => t.isNegative()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     visit(f: (TagsFilter: any) => void) { |     visit(f: (TagsFilter: any) => void) { | ||||||
|         f(this) |         f(this) | ||||||
|         this.or.forEach(t => t.visit(f)) |         this.or.forEach((t) => t.visit(f)) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -1,88 +1,87 @@ | ||||||
| import {Tag} from "./Tag"; | import { Tag } from "./Tag" | ||||||
| import {TagsFilter} from "./TagsFilter"; | import { TagsFilter } from "./TagsFilter" | ||||||
| 
 | 
 | ||||||
| export class RegexTag extends TagsFilter { | export class RegexTag extends TagsFilter { | ||||||
|     public readonly key: RegExp | string; |     public readonly key: RegExp | string | ||||||
|     public readonly value: RegExp | string; |     public readonly value: RegExp | string | ||||||
|     public readonly invert: boolean; |     public readonly invert: boolean | ||||||
|     public readonly matchesEmpty: boolean |     public readonly matchesEmpty: boolean | ||||||
| 
 | 
 | ||||||
|     constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) { |     constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) { | ||||||
|         super(); |         super() | ||||||
|         this.key = key; |         this.key = key | ||||||
|         this.value = value; |         this.value = value | ||||||
|         this.invert = invert; |         this.invert = invert | ||||||
|         this.matchesEmpty = RegexTag.doesMatch("", this.value); |         this.matchesEmpty = RegexTag.doesMatch("", this.value) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean { |     private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean { | ||||||
|         if (fromTag === undefined) { |         if (fromTag === undefined) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         if (typeof fromTag === "number") { |         if (typeof fromTag === "number") { | ||||||
|             fromTag = "" + fromTag; |             fromTag = "" + fromTag | ||||||
|         } |         } | ||||||
|         if (typeof possibleRegex === "string") { |         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) { |     private static source(r: string | RegExp) { | ||||||
|         if (typeof (r) === "string") { |         if (typeof r === "string") { | ||||||
|             return r; |             return r | ||||||
|         } |         } | ||||||
|         return r.source; |         return r.source | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * new RegexTag("a", /^[xyz]$/).asOverpass() // => [ `["a"~"^[xyz]$"]` ]
 |      * new RegexTag("a", /^[xyz]$/).asOverpass() // => [ `["a"~"^[xyz]$"]` ]
 | ||||||
|      *  |      * | ||||||
|      * // A wildcard regextag should only give the key
 |      * // A wildcard regextag should only give the key
 | ||||||
|      * new RegexTag("a", /^..*$/).asOverpass() // => [ `["a"]` ]
 |      * new RegexTag("a", /^..*$/).asOverpass() // => [ `["a"]` ]
 | ||||||
|      *  |      * | ||||||
|      * // A regextag with a regex key should give correct output
 |      * // A regextag with a regex key should give correct output
 | ||||||
|      * new RegexTag(/a.*x/, /^..*$/).asOverpass() // => [ `[~"a.*x"~\"^..*$\"]` ]
 |      * new RegexTag(/a.*x/, /^..*$/).asOverpass() // => [ `[~"a.*x"~\"^..*$\"]` ]
 | ||||||
|      *  |      * | ||||||
|      * // A regextag with a case invariant flag should signal this to overpass
 |      * // A regextag with a case invariant flag should signal this to overpass
 | ||||||
|      * new RegexTag("key", /^.*value.*$/i).asOverpass() // => [ `["key"~\"^.*value.*$\",i]` ]
 |      * new RegexTag("key", /^.*value.*$/i).asOverpass() // => [ `["key"~\"^.*value.*$\",i]` ]
 | ||||||
|      */ |      */ | ||||||
|     asOverpass(): string[] { |     asOverpass(): string[] { | ||||||
|         const inv =this.invert ? "!" : "" |         const inv = this.invert ? "!" : "" | ||||||
|         if (typeof this.key !== "string") { |         if (typeof this.key !== "string") { | ||||||
|             // The key is a regex too
 |             // 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){ |         if (this.value instanceof RegExp) { | ||||||
|             const src =this.value.source |             const src = this.value.source | ||||||
|             if(src === "^..*$"){ |             if (src === "^..*$") { | ||||||
|                 // anything goes
 |                 // anything goes
 | ||||||
|                 return [`[${inv}"${this.key}"]`] |                 return [`[${inv}"${this.key}"]`] | ||||||
|             } |             } | ||||||
|             const modifier = this.value.ignoreCase ? ",i" : "" |             const modifier = this.value.ignoreCase ? ",i" : "" | ||||||
|             return [`["${this.key}"${inv}~"${src}"${modifier}]`] |             return [`["${this.key}"${inv}~"${src}"${modifier}]`] | ||||||
|         }else{ |         } else { | ||||||
|             // Normal key and normal value
 |             // Normal key and normal value
 | ||||||
|             return [`["${this.key}"${inv}="${this.value}"]`]; |             return [`["${this.key}"${inv}="${this.value}"]`] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isUsableAsAnswer(): boolean { |     isUsableAsAnswer(): boolean { | ||||||
|         return false; |         return false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /**  |     /** | ||||||
|      * Checks if this tag matches the given properties |      * Checks if this tag matches the given properties | ||||||
|      *  |      * | ||||||
|      * const isNotEmpty = new RegexTag("key",/^$/, true); |      * const isNotEmpty = new RegexTag("key",/^$/, true); | ||||||
|      * isNotEmpty.matchesProperties({"key": "value"}) // => true
 |      * isNotEmpty.matchesProperties({"key": "value"}) // => true
 | ||||||
|      * isNotEmpty.matchesProperties({"key": "other_value"}) // => true
 |      * isNotEmpty.matchesProperties({"key": "other_value"}) // => true
 | ||||||
|      * isNotEmpty.matchesProperties({"key": ""}) // => false
 |      * isNotEmpty.matchesProperties({"key": ""}) // => false
 | ||||||
|      * isNotEmpty.matchesProperties({"other_key": ""}) // => false
 |      * isNotEmpty.matchesProperties({"other_key": ""}) // => false
 | ||||||
|      * isNotEmpty.matchesProperties({"other_key": "value"}) // => false
 |      * isNotEmpty.matchesProperties({"other_key": "value"}) // => false
 | ||||||
|      *  |      * | ||||||
|      * const isNotEmpty = new RegexTag("key",/^..*$/, true); |      * const isNotEmpty = new RegexTag("key",/^..*$/, true); | ||||||
|      * isNotEmpty.matchesProperties({"key": "value"}) // => false
 |      * isNotEmpty.matchesProperties({"key": "value"}) // => false
 | ||||||
|      * isNotEmpty.matchesProperties({"key": "other_value"}) // => false
 |      * isNotEmpty.matchesProperties({"key": "other_value"}) // => false
 | ||||||
|  | @ -95,7 +94,7 @@ export class RegexTag extends TagsFilter { | ||||||
|      * notRegex.matchesProperties({"x": "z"}) // => true
 |      * notRegex.matchesProperties({"x": "z"}) // => true
 | ||||||
|      * notRegex.matchesProperties({"x": ""}) // => true
 |      * notRegex.matchesProperties({"x": ""}) // => true
 | ||||||
|      * notRegex.matchesProperties({}) // => true
 |      * notRegex.matchesProperties({}) // => true
 | ||||||
|      *  |      * | ||||||
|      * const bicycleTubeRegex = new RegexTag("vending", /^.*bicycle_tube.*$/) |      * const bicycleTubeRegex = new RegexTag("vending", /^.*bicycle_tube.*$/) | ||||||
|      * bicycleTubeRegex.matchesProperties({"vending": "bicycle_tube"}) // => true
 |      * bicycleTubeRegex.matchesProperties({"vending": "bicycle_tube"}) // => true
 | ||||||
|      * bicycleTubeRegex.matchesProperties({"vending": "something;bicycle_tube"}) // => true
 |      * bicycleTubeRegex.matchesProperties({"vending": "something;bicycle_tube"}) // => true
 | ||||||
|  | @ -112,59 +111,59 @@ export class RegexTag extends TagsFilter { | ||||||
|      * notEmptyList.matchesProperties({"xyz": undefined}) // => true
 |      * notEmptyList.matchesProperties({"xyz": undefined}) // => true
 | ||||||
|      * notEmptyList.matchesProperties({"xyz": "[]"}) // => false
 |      * notEmptyList.matchesProperties({"xyz": "[]"}) // => false
 | ||||||
|      * notEmptyList.matchesProperties({"xyz": "[\"abc\"]"}) // => true
 |      * notEmptyList.matchesProperties({"xyz": "[\"abc\"]"}) // => true
 | ||||||
|      *  |      * | ||||||
|      * const importMatch = new RegexTag("tags", /(^|.*;)amenity=public_bookcase($|;.*)/) |      * const importMatch = new RegexTag("tags", /(^|.*;)amenity=public_bookcase($|;.*)/) | ||||||
|      * importMatch.matchesProperties({"tags": "amenity=public_bookcase;name=test"}) // =>true
 |      * importMatch.matchesProperties({"tags": "amenity=public_bookcase;name=test"}) // =>true
 | ||||||
|      * importMatch.matchesProperties({"tags": "amenity=public_bookcase"}) // =>true
 |      * importMatch.matchesProperties({"tags": "amenity=public_bookcase"}) // =>true
 | ||||||
|      * importMatch.matchesProperties({"tags": "name=test;amenity=public_bookcase"}) // =>true
 |      * importMatch.matchesProperties({"tags": "name=test;amenity=public_bookcase"}) // =>true
 | ||||||
|      * importMatch.matchesProperties({"tags": "amenity=bench"}) // =>false
 |      * importMatch.matchesProperties({"tags": "amenity=bench"}) // =>false
 | ||||||
|      *  |      * | ||||||
|      * new RegexTag("key","value").matchesProperties({"otherkey":"value"}) // => false
 |      * new RegexTag("key","value").matchesProperties({"otherkey":"value"}) // => false
 | ||||||
|      * new RegexTag("key","value",true).matchesProperties({"otherkey":"something"}) // => true
 |      * new RegexTag("key","value",true).matchesProperties({"otherkey":"something"}) // => true
 | ||||||
|      */ |      */ | ||||||
|     matchesProperties(tags: any): boolean { |     matchesProperties(tags: any): boolean { | ||||||
|         if (typeof this.key === "string") { |         if (typeof this.key === "string") { | ||||||
|             const value = tags[this.key] ?? "" |             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) { |         for (const key in tags) { | ||||||
|             if (key === undefined) { |             if (key === undefined) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
|             if (RegexTag.doesMatch(key, this.key)) { |             if (RegexTag.doesMatch(key, this.key)) { | ||||||
|                 const value = tags[key] ?? ""; |                 const value = tags[key] ?? "" | ||||||
|                 return RegexTag.doesMatch(value, this.value) != this.invert; |                 return RegexTag.doesMatch(value, this.value) != this.invert | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if (this.matchesEmpty) { |         if (this.matchesEmpty) { | ||||||
|             // The value is 'empty'
 |             // The value is 'empty'
 | ||||||
|             return !this.invert; |             return !this.invert | ||||||
|         } |         } | ||||||
|         // The matching key was not found
 |         // The matching key was not found
 | ||||||
|         return this.invert; |         return this.invert | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asHumanString() { |     asHumanString() { | ||||||
|         if (typeof this.key === "string") { |         if (typeof this.key === "string") { | ||||||
|             const oper = typeof this.value === "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)}` |         return `${this.key.source}${this.invert ? "!" : ""}~~${RegexTag.source(this.value)}` | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      *  |      * | ||||||
|      * new RegexTag("key","value").shadows(new Tag("key","value")) // => true
 |      * new RegexTag("key","value").shadows(new Tag("key","value")) // => true
 | ||||||
|      * new RegexTag("key",/value/).shadows(new RegexTag("key","value")) // => true
 |      * new RegexTag("key",/value/).shadows(new RegexTag("key","value")) // => true
 | ||||||
|      * new RegexTag("key",/^..*$/).shadows(new Tag("key","value")) // => false
 |      * new RegexTag("key",/^..*$/).shadows(new Tag("key","value")) // => false
 | ||||||
|      * new RegexTag("key",/^..*$/).shadows(new Tag("other_key","value")) // => false
 |      * new RegexTag("key",/^..*$/).shadows(new Tag("other_key","value")) // => false
 | ||||||
|      * new RegexTag("key", /^a+$/).shadows(new Tag("key", "a")) // => false
 |      * new RegexTag("key", /^a+$/).shadows(new Tag("key", "a")) // => false
 | ||||||
|      *  |      * | ||||||
|      *  |      * | ||||||
|      * // should not shadow too eagerly: the first tag might match 'key=abc', the second won't
 |      * // should not shadow too eagerly: the first tag might match 'key=abc', the second won't
 | ||||||
|      *  new RegexTag("key", /^..*$/).shadows(new Tag("key", "some_value")) // => false
 |      *  new RegexTag("key", /^..*$/).shadows(new Tag("key", "some_value")) // => false
 | ||||||
|      *  |      * | ||||||
|      * // should handle 'invert'
 |      * // should handle 'invert'
 | ||||||
|      * new RegexTag("key",/^..*$/, true).shadows(new Tag("key","value")) // => false
 |      * new RegexTag("key",/^..*$/, true).shadows(new Tag("key","value")) // => false
 | ||||||
|      * new RegexTag("key",/^..*$/, true).shadows(new Tag("key","")) // => true
 |      * new RegexTag("key",/^..*$/, true).shadows(new Tag("key","")) // => true
 | ||||||
|  | @ -173,50 +172,51 @@ export class RegexTag extends TagsFilter { | ||||||
|      */ |      */ | ||||||
|     shadows(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         if (other instanceof RegexTag) { |         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
 |                 // Keys don't match, never shadowing
 | ||||||
|                 return false |                 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
 |                 // Values (and inverts) match
 | ||||||
|                 return true |                 return true | ||||||
|             } |             } | ||||||
|             if(typeof other.value ==="string"){ |             if (typeof other.value === "string") { | ||||||
|                 const valuesMatch = RegexTag.doesMatch(other.value, this.value) |                 const valuesMatch = RegexTag.doesMatch(other.value, this.value) | ||||||
|                 if(!this.invert && !other.invert){ |                 if (!this.invert && !other.invert) { | ||||||
|                     // this: key~value, other: key=value
 |                     // this: key~value, other: key=value
 | ||||||
|                     return valuesMatch |                     return valuesMatch | ||||||
|                 } |                 } | ||||||
|                 if(this.invert && !other.invert){ |                 if (this.invert && !other.invert) { | ||||||
|                     // this: key!~value, other: key=value
 |                     // this: key!~value, other: key=value
 | ||||||
|                     return !valuesMatch |                     return !valuesMatch | ||||||
|                 } |                 } | ||||||
|                 if(!this.invert && other.invert){ |                 if (!this.invert && other.invert) { | ||||||
|                     // this: key~value, other: key!=value
 |                     // this: key~value, other: key!=value
 | ||||||
|                     return !valuesMatch |                     return !valuesMatch | ||||||
|                 } |                 } | ||||||
|                 if(!this.invert && !other.invert){ |                 if (!this.invert && !other.invert) { | ||||||
|                     // this: key!~value, other: key!=value
 |                     // this: key!~value, other: key!=value
 | ||||||
|                     return valuesMatch |                     return valuesMatch | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|             } |             } | ||||||
|             return false; |             return false | ||||||
|         } |         } | ||||||
|         if (other instanceof Tag) { |         if (other instanceof Tag) { | ||||||
|             if(!RegexTag.doesMatch(other.key, this.key)){ |             if (!RegexTag.doesMatch(other.key, this.key)) { | ||||||
|                 // Keys don't match
 |                 // Keys don't match
 | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|              | 
 | ||||||
|              |             if (this.value["source"] === "^..*$") { | ||||||
|             if(this.value["source"] === "^..*$") { |                 if (this.invert) { | ||||||
|                 if(this.invert){ |  | ||||||
|                     return other.value === "" |                     return other.value === "" | ||||||
|                 } |                 } | ||||||
|                 return false |                 return false | ||||||
|             } |             } | ||||||
|              | 
 | ||||||
|             if (this.invert) { |             if (this.invert) { | ||||||
|                 /* |                 /* | ||||||
|                  * this: "a!=b" |                  * this: "a!=b" | ||||||
|  | @ -224,23 +224,23 @@ export class RegexTag extends TagsFilter { | ||||||
|                  * actual property: a=x |                  * actual property: a=x | ||||||
|                  * In other words: shadowing will never occur here |                  * 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
 |             // 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[] { |     usedKeys(): string[] { | ||||||
|         if (typeof this.key === "string") { |         if (typeof this.key === "string") { | ||||||
|             return [this.key]; |             return [this.key] | ||||||
|         } |         } | ||||||
|         throw "Key cannot be determined as it is a regex" |         throw "Key cannot be determined as it is a regex" | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     usedTags(): { key: string; value: string }[] { |     usedTags(): { key: string; value: string }[] { | ||||||
|         return []; |         return [] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asChange(properties: any): { k: string; v: string }[] { |     asChange(properties: any): { k: string; v: string }[] { | ||||||
|  | @ -249,26 +249,26 @@ export class RegexTag extends TagsFilter { | ||||||
|         } |         } | ||||||
|         if (typeof this.key === "string") { |         if (typeof this.key === "string") { | ||||||
|             if (typeof this.value === "string") { |             if (typeof this.value === "string") { | ||||||
|                 return [{k: this.key, v: this.value}] |                 return [{ k: this.key, v: this.value }] | ||||||
|             } |             } | ||||||
|             if (this.value.toString() != "/^..*$/") { |             if (this.value.toString() != "/^..*$/") { | ||||||
|                 console.warn("Regex value in tag; using wildcard:", this.key, this.value) |                 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) |         console.error("Cannot export regex tag to asChange; ", this.key, this.value) | ||||||
|         return [] |         return [] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): TagsFilter | boolean { | ||||||
|         return this; |         return this | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|         return this.invert; |         return this.invert | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     visit(f: (TagsFilter) => void) { |     visit(f: (TagsFilter) => void) { | ||||||
|         f(this) |         f(this) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import {TagsFilter} from "./TagsFilter"; | import { TagsFilter } from "./TagsFilter" | ||||||
| import {Tag} from "./Tag"; | import { Tag } from "./Tag" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The substituting-tag uses the tags of a feature a variables and replaces them. |  * 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 |  * This cannot be used to query features | ||||||
|  */ |  */ | ||||||
| export default class SubstitutingTag implements TagsFilter { | export default class SubstitutingTag implements TagsFilter { | ||||||
|     private readonly _key: string; |     private readonly _key: string | ||||||
|     private readonly _value: string; |     private readonly _value: string | ||||||
|     private readonly _invert: boolean |     private readonly _invert: boolean | ||||||
| 
 | 
 | ||||||
|     constructor(key: string, value: string, invert = false) { |     constructor(key: string, value: string, invert = false) { | ||||||
|         this._key = key; |         this._key = key | ||||||
|         this._value = value; |         this._value = value | ||||||
|         this._invert = invert |         this._invert = invert | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static substituteString(template: string, dict: any): string { |     private static substituteString(template: string, dict: any): string { | ||||||
|         for (const k in dict) { |         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>){ |     asTag(currentProperties: Record<string, string>) { | ||||||
|         if(this._invert){ |         if (this._invert) { | ||||||
|             throw "Cannot convert an inverted substituting tag" |             throw "Cannot convert an inverted substituting tag" | ||||||
|         } |         } | ||||||
|         return new Tag(this._key, Utils.SubstituteKeys(this._value, currentProperties)) |         return new Tag(this._key, Utils.SubstituteKeys(this._value, currentProperties)) | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     asHumanString(linkToWiki: boolean, shorten: boolean, properties) { |     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[] { |     asOverpass(): string[] { | ||||||
|  | @ -46,13 +51,17 @@ export default class SubstitutingTag implements TagsFilter { | ||||||
| 
 | 
 | ||||||
|     shadows(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         if (!(other instanceof SubstitutingTag)) { |         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 { |     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
 |      * assign.matchesProperties({"some_key": "2021-03-29"}) // => false
 | ||||||
|      */ |      */ | ||||||
|     matchesProperties(properties: any): boolean { |     matchesProperties(properties: any): boolean { | ||||||
|         const value = properties[this._key]; |         const value = properties[this._key] | ||||||
|         if (value === undefined || value === "") { |         if (value === undefined || value === "") { | ||||||
|             return false; |             return false | ||||||
|         } |         } | ||||||
|         const expectedValue = SubstitutingTag.substituteString(this._value, properties); |         const expectedValue = SubstitutingTag.substituteString(this._value, properties) | ||||||
|         return (value === expectedValue) !== this._invert; |         return (value === expectedValue) !== this._invert | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     usedKeys(): string[] { |     usedKeys(): string[] { | ||||||
|         return [this._key]; |         return [this._key] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     usedTags(): { key: string; value: string }[] { |     usedTags(): { key: string; value: string }[] { | ||||||
|  | @ -84,22 +93,22 @@ export default class SubstitutingTag implements TagsFilter { | ||||||
|         if (this._invert) { |         if (this._invert) { | ||||||
|             throw "An inverted substituting tag can not be used to create a change" |             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) { |         if (v.match(/{.*}/) !== null) { | ||||||
|             throw "Could not calculate all the substitutions: still have " + v |             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 { |     optimize(): TagsFilter | boolean { | ||||||
|         return this; |         return this | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|         return false; |         return false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     visit(f: (TagsFilter: any) => void) { |     visit(f: (TagsFilter: any) => void) { | ||||||
|         f(this) |         f(this) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import {TagsFilter} from "./TagsFilter"; | import { TagsFilter } from "./TagsFilter" | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| export class Tag extends TagsFilter { | export class Tag extends TagsFilter { | ||||||
|     public key: string |     public key: string | ||||||
|  | @ -10,56 +9,57 @@ export class Tag extends TagsFilter { | ||||||
|         this.key = key |         this.key = key | ||||||
|         this.value = value |         this.value = value | ||||||
|         if (key === undefined || key === "") { |         if (key === undefined || key === "") { | ||||||
|             throw "Invalid key: undefined or empty"; |             throw "Invalid key: undefined or empty" | ||||||
|         } |         } | ||||||
|         if (value === undefined) { |         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 === "*") { |         if (value === "*") { | ||||||
|             console.warn(`Got suspicious tag ${key}=*   ; did you mean ${key}~* ?`) |             console.warn(`Got suspicious tag ${key}=*   ; did you mean ${key}~* ?`) | ||||||
|         } |         } | ||||||
|         if(value.indexOf("&") >= 0){ |         if (value.indexOf("&") >= 0) { | ||||||
|             const tags = (key + "="+value).split("&") |             const tags = (key + "=" + value).split("&") | ||||||
|             throw `Invalid value for a tag: it contains '&'. You probably meant to use '{"and":[${tags.map(kv => "\"" + kv +"\"").join(', ')}]}'` |             throw `Invalid value for a tag: it contains '&'. You probably meant to use '{"and":[${tags | ||||||
|  |                 .map((kv) => '"' + kv + '"') | ||||||
|  |                 .join(", ")}]}'` | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * imort  |      * imort | ||||||
|      *  |      * | ||||||
|      * const tag = new Tag("key","value") |      * const tag = new Tag("key","value") | ||||||
|      * tag.matchesProperties({"key": "value"}) // =>  true
 |      * tag.matchesProperties({"key": "value"}) // =>  true
 | ||||||
|      * tag.matchesProperties({"key": "z"}) // =>  false
 |      * tag.matchesProperties({"key": "z"}) // =>  false
 | ||||||
|      * tag.matchesProperties({"key": ""}) // => false
 |      * tag.matchesProperties({"key": ""}) // => false
 | ||||||
|      * tag.matchesProperties({"other_key": ""}) // => false
 |      * tag.matchesProperties({"other_key": ""}) // => false
 | ||||||
|      * tag.matchesProperties({"other_key": "value"}) // =>  false
 |      * tag.matchesProperties({"other_key": "value"}) // =>  false
 | ||||||
|      *  |      * | ||||||
|      * const isEmpty = new Tag("key","") |      * const isEmpty = new Tag("key","") | ||||||
|      * isEmpty.matchesProperties({"key": "value"}) // => false
 |      * isEmpty.matchesProperties({"key": "value"}) // => false
 | ||||||
|      * isEmpty.matchesProperties({"key": ""}) // => true
 |      * isEmpty.matchesProperties({"key": ""}) // => true
 | ||||||
|      * isEmpty.matchesProperties({"other_key": ""}) // => true
 |      * isEmpty.matchesProperties({"other_key": ""}) // => true
 | ||||||
|      * isEmpty.matchesProperties({"other_key": "value"}) // => true
 |      * isEmpty.matchesProperties({"other_key": "value"}) // => true
 | ||||||
|      * isEmpty.matchesProperties({"key": undefined}) // => true
 |      * isEmpty.matchesProperties({"key": undefined}) // => true
 | ||||||
|      *  |      * | ||||||
|      */ |      */ | ||||||
|     matchesProperties(properties: any): boolean { |     matchesProperties(properties: any): boolean { | ||||||
|         const foundValue = properties[this.key] |         const foundValue = properties[this.key] | ||||||
|         if (foundValue === undefined && (this.value === "" || this.value === undefined)) { |         if (foundValue === undefined && (this.value === "" || this.value === undefined)) { | ||||||
|             // The tag was not found
 |             // The tag was not found
 | ||||||
|             // and it shouldn't be found!
 |             // and it shouldn't be found!
 | ||||||
|             return true; |             return true | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return foundValue === this.value; |         return foundValue === this.value | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asOverpass(): string[] { |     asOverpass(): string[] { | ||||||
|         if (this.value === "") { |         if (this.value === "") { | ||||||
|             // NOT having this key
 |             // 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>"
 |      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) { |     asHumanString(linkToWiki?: boolean, shorten?: boolean, currentProperties?: any) { | ||||||
|         let v = this.value; |         let v = this.value | ||||||
|         if (shorten) { |         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
 |             // This tag will be removed if in the properties, so we indicate this with special rendering
 | ||||||
|             if (currentProperties !== undefined && (currentProperties[this.key] ?? "") === "") { |             if (currentProperties !== undefined && (currentProperties[this.key] ?? "") === "") { | ||||||
|                 // This tag is not present in the current properties, so this tag doesn't change anything
 |                 // This tag is not present in the current properties, so this tag doesn't change anything
 | ||||||
|  | @ -82,21 +82,23 @@ export class Tag extends TagsFilter { | ||||||
|             return "<span class='line-through'>" + this.key + "</span>" |             return "<span class='line-through'>" + this.key + "</span>" | ||||||
|         } |         } | ||||||
|         if (linkToWiki) { |         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>` |                 `<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 { |     isUsableAsAnswer(): boolean { | ||||||
|         return true; |         return true | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      *  |      * | ||||||
|      * import {RegexTag} from "./RegexTag"; |      * import {RegexTag} from "./RegexTag"; | ||||||
|      *  |      * | ||||||
|      * // should handle advanced regexes
 |      * // should handle advanced regexes
 | ||||||
|      * new Tag("key", "aaa").shadows(new RegexTag("key", /a+/)) // => true
 |      * new Tag("key", "aaa").shadows(new RegexTag("key", /a+/)) // => true
 | ||||||
|      * new Tag("key","value").shadows(new RegexTag("key", /^..*$/, true)) // => false
 |      * new Tag("key","value").shadows(new RegexTag("key", /^..*$/, true)) // => false
 | ||||||
|  | @ -107,38 +109,38 @@ export class Tag extends TagsFilter { | ||||||
|      * new Tag("key","value").shadows(new RegexTag("otherkey", "value", false)) // => false
 |      * new Tag("key","value").shadows(new RegexTag("otherkey", "value", false)) // => false
 | ||||||
|      */ |      */ | ||||||
|     shadows(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         if(other["key"] !== undefined){ |         if (other["key"] !== undefined) { | ||||||
|             if(other["key"] !== this.key){ |             if (other["key"] !== this.key) { | ||||||
|                 return false |                 return false | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return other.matchesProperties({[this.key]: this.value}); |         return other.matchesProperties({ [this.key]: this.value }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     usedKeys(): string[] { |     usedKeys(): string[] { | ||||||
|         return [this.key]; |         return [this.key] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     usedTags(): { key: string; value: string }[] { |     usedTags(): { key: string; value: string }[] { | ||||||
|         if(this.value == ""){ |         if (this.value == "") { | ||||||
|             return [] |             return [] | ||||||
|         } |         } | ||||||
|         return [this] |         return [this] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asChange(properties: any): { k: string; v: string }[] { |     asChange(properties: any): { k: string; v: string }[] { | ||||||
|         return [{k: this.key, v: this.value}]; |         return [{ k: this.key, v: this.value }] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): TagsFilter | boolean { | ||||||
|         return this; |         return this | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|         return false; |         return false | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     visit(f: (TagsFilter) => void) { |     visit(f: (TagsFilter) => void) { | ||||||
|         f(this) |         f(this) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,23 +1,21 @@ | ||||||
| import {Tag} from "./Tag"; | import { Tag } from "./Tag" | ||||||
| import {TagsFilter} from "./TagsFilter"; | import { TagsFilter } from "./TagsFilter" | ||||||
| import {And} from "./And"; | import { And } from "./And" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import ComparingTag from "./ComparingTag"; | import ComparingTag from "./ComparingTag" | ||||||
| import {RegexTag} from "./RegexTag"; | import { RegexTag } from "./RegexTag" | ||||||
| import SubstitutingTag from "./SubstitutingTag"; | import SubstitutingTag from "./SubstitutingTag" | ||||||
| import {Or} from "./Or"; | import { Or } from "./Or" | ||||||
| import {TagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson"; | import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
| import {isRegExp} from "util"; | import { isRegExp } from "util" | ||||||
| import * as key_counts from "../../assets/key_totals.json" | import * as key_counts from "../../assets/key_totals.json" | ||||||
| 
 | 
 | ||||||
| type Tags = Record<string, string> | type Tags = Record<string, string> | ||||||
| export type UploadableTag  = Tag | SubstitutingTag | And | export type UploadableTag = Tag | SubstitutingTag | And | ||||||
| 
 | 
 | ||||||
| export class TagUtils { | export class TagUtils { | ||||||
|     private static keyCounts: { keys: any, tags: any } = key_counts["default"] ?? key_counts |     private static keyCounts: { keys: any; tags: any } = key_counts["default"] ?? key_counts | ||||||
|     private static comparators |     private static comparators: [string, (a: number, b: number) => boolean][] = [ | ||||||
|         : [string, (a: number, b: number) => boolean][] |  | ||||||
|         = [ |  | ||||||
|         ["<=", (a, b) => a <= b], |         ["<=", (a, b) => a <= b], | ||||||
|         [">=", (a, b) => a >= b], |         [">=", (a, b) => a >= b], | ||||||
|         ["<", (a, b) => a < b], |         ["<", (a, b) => a < b], | ||||||
|  | @ -25,14 +23,14 @@ export class TagUtils { | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     static KVtoProperties(tags: Tag[]): any { |     static KVtoProperties(tags: Tag[]): any { | ||||||
|         const properties = {}; |         const properties = {} | ||||||
|         for (const tag of tags) { |         for (const tag of tags) { | ||||||
|             properties[tag.key] = tag.value |             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 = {} |         const tags = {} | ||||||
|         for (const kv of kvs) { |         for (const kv of kvs) { | ||||||
|             tags[kv.k] = kv.v |             tags[kv.k] = kv.v | ||||||
|  | @ -47,20 +45,20 @@ export class TagUtils { | ||||||
|         for (const neededKey in neededTags) { |         for (const neededKey in neededTags) { | ||||||
|             const availableValues: string[] = availableTags[neededKey] |             const availableValues: string[] = availableTags[neededKey] | ||||||
|             if (availableValues === undefined) { |             if (availableValues === undefined) { | ||||||
|                 return false; |                 return false | ||||||
|             } |             } | ||||||
|             const neededValues: string[] = neededTags[neededKey]; |             const neededValues: string[] = neededTags[neededKey] | ||||||
|             for (const neededValue of neededValues) { |             for (const neededValue of neededValues) { | ||||||
|                 if (availableValues.indexOf(neededValue) < 0) { |                 if (availableValues.indexOf(neededValue) < 0) { | ||||||
|                     return false; |                     return false | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return true; |         return true | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static SplitKeys(tagsFilters: UploadableTag[]): Record<string, string[]> { |     static SplitKeys(tagsFilters: UploadableTag[]): Record<string, string[]> { | ||||||
|         return <any>this.SplitKeysRegex(tagsFilters, false); |         return <any>this.SplitKeysRegex(tagsFilters, false) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /*** |     /*** | ||||||
|  | @ -68,69 +66,72 @@ export class TagUtils { | ||||||
|      * |      * | ||||||
|      * TagUtils.SplitKeysRegex([new Tag("isced:level", "bachelor; master")], true) // => {"isced:level": ["bachelor","master"]}
 |      * 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)[]> = {} |         const keyValues: Record<string, (string | RegexTag)[]> = {} | ||||||
|         tagsFilters = [...tagsFilters] // copy all, use as queue
 |         tagsFilters = [...tagsFilters] // copy all, use as queue
 | ||||||
|         while (tagsFilters.length > 0) { |         while (tagsFilters.length > 0) { | ||||||
|             const tagsFilter = tagsFilters.shift(); |             const tagsFilter = tagsFilters.shift() | ||||||
| 
 | 
 | ||||||
|             if (tagsFilter === undefined) { |             if (tagsFilter === undefined) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (tagsFilter instanceof And) { |             if (tagsFilter instanceof And) { | ||||||
|                 tagsFilters.push(...<UploadableTag[]>tagsFilter.and); |                 tagsFilters.push(...(<UploadableTag[]>tagsFilter.and)) | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (tagsFilter instanceof Tag) { |             if (tagsFilter instanceof Tag) { | ||||||
|                 if (keyValues[tagsFilter.key] === undefined) { |                 if (keyValues[tagsFilter.key] === undefined) { | ||||||
|                     keyValues[tagsFilter.key] = []; |                     keyValues[tagsFilter.key] = [] | ||||||
|                 } |                 } | ||||||
|                 keyValues[tagsFilter.key].push(...tagsFilter.value.split(";").map(s => s.trim())); |                 keyValues[tagsFilter.key].push(...tagsFilter.value.split(";").map((s) => s.trim())) | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (allowRegex && tagsFilter instanceof RegexTag) { |             if (allowRegex && tagsFilter instanceof RegexTag) { | ||||||
|                 const key = tagsFilter.key |                 const key = tagsFilter.key | ||||||
|                 if (isRegExp(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" |                     throw "Invalid type to FlattenMultiAnswer" | ||||||
|                 } |                 } | ||||||
|                 const keystr = <string>key |                 const keystr = <string>key | ||||||
|                 if (keyValues[keystr] === undefined) { |                 if (keyValues[keystr] === undefined) { | ||||||
|                     keyValues[keystr] = []; |                     keyValues[keystr] = [] | ||||||
|                 } |                 } | ||||||
|                 keyValues[keystr].push(tagsFilter); |                 keyValues[keystr].push(tagsFilter) | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| 
 |             console.error("Invalid type to flatten the multiAnswer", tagsFilter) | ||||||
|             console.error("Invalid type to flatten the multiAnswer", tagsFilter); |  | ||||||
|             throw "Invalid type to FlattenMultiAnswer" |             throw "Invalid type to FlattenMultiAnswer" | ||||||
|         } |         } | ||||||
|         return keyValues; |         return keyValues | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Flattens an 'uploadableTag' and replaces all 'SubstitutingTags' into normal tags |      * Flattens an 'uploadableTag' and replaces all 'SubstitutingTags' into normal tags | ||||||
|      */ |      */ | ||||||
|     static FlattenAnd(tagFilters: UploadableTag, currentProperties: Record<string, string>): Tag[]{ |     static FlattenAnd(tagFilters: UploadableTag, currentProperties: Record<string, string>): Tag[] { | ||||||
|         const tags : Tag[] = [] |         const tags: Tag[] = [] | ||||||
|         tagFilters.visit((tf: UploadableTag) => { |         tagFilters.visit((tf: UploadableTag) => { | ||||||
|             if(tf instanceof Tag){ |             if (tf instanceof Tag) { | ||||||
|                 tags.push(tf) |                 tags.push(tf) | ||||||
|             } |             } | ||||||
|             if(tf instanceof SubstitutingTag){ |             if (tf instanceof SubstitutingTag) { | ||||||
|                 tags.push(tf.asTag(currentProperties)) |                 tags.push(tf.asTag(currentProperties)) | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|         return tags |         return tags | ||||||
|     } |     } | ||||||
|      |  | ||||||
|   |  | ||||||
| 
 | 
 | ||||||
|         /** |     /** | ||||||
|      * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set. |      * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set. | ||||||
|      * E.g: |      * E.g: | ||||||
|      * |      * | ||||||
|  | @ -152,17 +153,17 @@ export class TagUtils { | ||||||
|      */ |      */ | ||||||
|     static FlattenMultiAnswer(tagsFilters: UploadableTag[]): And { |     static FlattenMultiAnswer(tagsFilters: UploadableTag[]): And { | ||||||
|         if (tagsFilters === undefined) { |         if (tagsFilters === undefined) { | ||||||
|             return new And([]); |             return new And([]) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let keyValues = TagUtils.SplitKeys(tagsFilters); |         let keyValues = TagUtils.SplitKeys(tagsFilters) | ||||||
|         const and: UploadableTag[] = [] |         const and: UploadableTag[] = [] | ||||||
|         for (const key in keyValues) { |         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() |             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
 |      * TagUtils.MatchesMultiAnswer(new Tag("isced:level","master"), {"isced:level":"bachelor; master"}) // => true
 | ||||||
|      */ |      */ | ||||||
|     static MatchesMultiAnswer(tag: UploadableTag, properties: Tags): boolean { |     static MatchesMultiAnswer(tag: UploadableTag, properties: Tags): boolean { | ||||||
|         const splitted = TagUtils.SplitKeysRegex([tag], true); |         const splitted = TagUtils.SplitKeysRegex([tag], true) | ||||||
|         for (const splitKey in splitted) { |         for (const splitKey in splitted) { | ||||||
|             const neededValues = splitted[splitKey]; |             const neededValues = splitted[splitKey] | ||||||
|             if (properties[splitKey] === undefined) { |             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) { |             for (const neededValue of neededValues) { | ||||||
| 
 |  | ||||||
|                 if (neededValue instanceof RegexTag) { |                 if (neededValue instanceof RegexTag) { | ||||||
|                     if (!neededValue.matchesProperties(properties)) { |                     if (!neededValue.matchesProperties(properties)) { | ||||||
|                         return false |                         return false | ||||||
|  | @ -194,19 +194,19 @@ export class TagUtils { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|                 if (actualValue.indexOf(neededValue) < 0) { |                 if (actualValue.indexOf(neededValue) < 0) { | ||||||
|                     return false; |                     return false | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return true; |         return true | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static SimpleTag(json: string, context?: string): Tag { |     public static SimpleTag(json: string, context?: string): Tag { | ||||||
|         const tag = Utils.SplitFirst(json, "="); |         const tag = Utils.SplitFirst(json, "=") | ||||||
|         if (tag.length !== 2) { |         if (tag.length !== 2) { | ||||||
|             throw `Invalid tag: no (or too much) '=' found (in ${context ?? "unkown context"})` |             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 { |     public static Tag(json: TagConfigJson, context: string = ""): TagsFilter { | ||||||
|         try { |         try { | ||||||
|             return this.ParseTagUnsafe(json, context); |             return this.ParseTagUnsafe(json, context) | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.error("Could not parse tag", json, "in context", context, "due to ", 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 { |     public static ParseUploadableTag(json: TagConfigJson, context: string = ""): UploadableTag { | ||||||
|             const t = this.Tag(json, context); |         const t = this.Tag(json, context) | ||||||
|              | 
 | ||||||
|             t.visit((t : TagsFilter)=> { |         t.visit((t: TagsFilter) => { | ||||||
|                 if( t instanceof  And){ |             if (t instanceof And) { | ||||||
|                     return |                 return | ||||||
|                 } |             } | ||||||
|                 if(t instanceof Tag){ |             if (t instanceof Tag) { | ||||||
|                     return |                 return | ||||||
|                 } |             } | ||||||
|                 if(t instanceof SubstitutingTag){ |             if (t instanceof SubstitutingTag) { | ||||||
|                     return |                 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) |         return TagUtils.Tag(json, context) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * INLINE sort of the given list |      * 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)) |         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: ""}
 |      * TagUtils.parseRegexOperator("tileId~*") // => {invert: false, key: "tileId", value: "*", modifier: ""}
 | ||||||
|      */ |      */ | ||||||
|     public static parseRegexOperator(tag: string): { |     public static parseRegexOperator(tag: string): { | ||||||
|         invert: boolean; |         invert: boolean | ||||||
|         key: string; |         key: string | ||||||
|         value: string; |         value: string | ||||||
|         modifier: "i" | ""; |         modifier: "i" | "" | ||||||
|     } | null { |     } | null { | ||||||
|         const match = tag.match(/^([_a-zA-Z0-9: -]+)(!)?~([i]~)?(.*)$/); |         const match = tag.match(/^([_a-zA-Z0-9: -]+)(!)?~([i]~)?(.*)$/) | ||||||
|         if (match == null) { |         if (match == null) { | ||||||
|             return null; |             return null | ||||||
|         } |         } | ||||||
|         const [ , key, invert, modifier, value] = match; |         const [, key, invert, modifier, value] = match | ||||||
|         return {key, value, invert: invert == "!", modifier: (modifier == "i~" ? "i" : "")}; |         return { key, value, invert: invert == "!", modifier: modifier == "i~" ? "i" : "" } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilter { |     private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilter { | ||||||
| 
 |  | ||||||
|         if (json === undefined) { |         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) { |             if (json["and"] !== undefined && json["or"] !== undefined) { | ||||||
|                 throw `Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined` |                 throw `Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined` | ||||||
|             } |             } | ||||||
|             if (json["and"] !== undefined) { |             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) { |             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)}` |             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) { |         for (const [operator, comparator] of TagUtils.comparators) { | ||||||
|             if (tag.indexOf(operator) >= 0) { |             if (tag.indexOf(operator) >= 0) { | ||||||
|                 const split = Utils.SplitFirst(tag, operator); |                 const split = Utils.SplitFirst(tag, operator) | ||||||
| 
 | 
 | ||||||
|                 let val = Number(split[1].trim()) |                 let val = Number(split[1].trim()) | ||||||
|                 if (isNaN(val)) { |                 if (isNaN(val)) { | ||||||
|  | @ -390,7 +393,7 @@ export class TagUtils { | ||||||
| 
 | 
 | ||||||
|                 const f = (value: string | number | undefined) => { |                 const f = (value: string | number | undefined) => { | ||||||
|                     if (value === undefined) { |                     if (value === undefined) { | ||||||
|                         return false; |                         return false | ||||||
|                     } |                     } | ||||||
|                     let b: number |                     let b: number | ||||||
|                     if (typeof value === "number") { |                     if (typeof value === "number") { | ||||||
|  | @ -413,14 +416,14 @@ export class TagUtils { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (tag.indexOf("~~") >= 0) { |         if (tag.indexOf("~~") >= 0) { | ||||||
|             const split = Utils.SplitFirst(tag, "~~"); |             const split = Utils.SplitFirst(tag, "~~") | ||||||
|             if (split[1] === "*") { |             if (split[1] === "*") { | ||||||
|                 split[1] = "..*" |                 split[1] = "..*" | ||||||
|             } |             } | ||||||
|             return new RegexTag( |             return new RegexTag( | ||||||
|                 new RegExp("^" + split[0] + "$"), |                 new RegExp("^" + split[0] + "$"), | ||||||
|                 new RegExp("^" + split[1] + "$", "s") |                 new RegExp("^" + split[1] + "$", "s") | ||||||
|             ); |             ) | ||||||
|         } |         } | ||||||
|         const withRegex = TagUtils.parseRegexOperator(tag) |         const withRegex = TagUtils.parseRegexOperator(tag) | ||||||
|         if (withRegex != null) { |         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})` |                 throw `Don't use 'key!~*' - use 'key=' instead (empty string as value (in the tag ${tag} while parsing ${context})` | ||||||
|             } |             } | ||||||
|             if (withRegex.value === "") { |             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 === "*") { |             if (value === "*") { | ||||||
|                 value = "..*" |                 value = "..*" | ||||||
|             } |             } | ||||||
|  | @ -439,39 +448,40 @@ export class TagUtils { | ||||||
|                 withRegex.key, |                 withRegex.key, | ||||||
|                 new RegExp("^" + value + "$", "s" + withRegex.modifier), |                 new RegExp("^" + value + "$", "s" + withRegex.modifier), | ||||||
|                 withRegex.invert |                 withRegex.invert | ||||||
|             ); |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (tag.indexOf("!:=") >= 0) { |         if (tag.indexOf("!:=") >= 0) { | ||||||
|             const split = Utils.SplitFirst(tag, "!:="); |             const split = Utils.SplitFirst(tag, "!:=") | ||||||
|             return new SubstitutingTag(split[0], split[1], true); |             return new SubstitutingTag(split[0], split[1], true) | ||||||
|         } |         } | ||||||
|         if (tag.indexOf(":=") >= 0) { |         if (tag.indexOf(":=") >= 0) { | ||||||
|             const split = Utils.SplitFirst(tag, ":="); |             const split = Utils.SplitFirst(tag, ":=") | ||||||
|             return new SubstitutingTag(split[0], split[1]); |             return new SubstitutingTag(split[0], split[1]) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (tag.indexOf("!=") >= 0) { |         if (tag.indexOf("!=") >= 0) { | ||||||
|             const split = Utils.SplitFirst(tag, "!="); |             const split = Utils.SplitFirst(tag, "!=") | ||||||
|             if (split[1] === "*") { |             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] === "") { |             if (split[1] === "") { | ||||||
|                 split[1] = "..*" |                 split[1] = "..*" | ||||||
|                 return new RegexTag(split[0], /^..*$/s) |                 return new RegexTag(split[0], /^..*$/s) | ||||||
|             } |             } | ||||||
|             return new RegexTag( |             return new RegexTag(split[0], split[1], true) | ||||||
|                 split[0], |  | ||||||
|                 split[1], |  | ||||||
|                 true |  | ||||||
|             ); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         if (tag.indexOf("=") >= 0) { |         if (tag.indexOf("=") >= 0) { | ||||||
| 
 |             const split = Utils.SplitFirst(tag, "=") | ||||||
| 
 |  | ||||||
|             const split = Utils.SplitFirst(tag, "="); |  | ||||||
|             if (split[1] == "*") { |             if (split[1] == "*") { | ||||||
|                 throw `Error while parsing tag '${tag}' in ${context}: detected a wildcard on a normal value. Use a regex pattern instead` |                 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) { |     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) { |         if (toplevel) { | ||||||
|             return joined |             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
 | ||||||
|      * 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++) { |         for (let i = 0; i < tags.length; i++) { | ||||||
|             const tag = tags[i]; |             const tag = tags[i] | ||||||
|             if (!(tag instanceof Tag || tag instanceof RegexTag)) { |             if (!(tag instanceof Tag || tag instanceof RegexTag)) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             for (let j = i + 1; j < tags.length; j++) { |             for (let j = i + 1; j < tags.length; j++) { | ||||||
|                 const guard = tags[j]; |                 const guard = tags[j] | ||||||
|                 if (!(guard instanceof Tag || guard instanceof RegexTag)) { |                 if (!(guard instanceof Tag || guard instanceof RegexTag)) { | ||||||
|                     continue |                     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")]
 |      * 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[] { |     public static removeShadowedElementsFrom( | ||||||
|         return listToFilter.filter(tf => !blacklist.some(guard => guard.shadows(tf))) |         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[] { |     public static removeEquivalents(listToFilter: (Tag | RegexTag)[]): TagsFilter[] { | ||||||
|         const result: TagsFilter[] = [] |         const result: TagsFilter[] = [] | ||||||
|         outer: for (let i = 0; i < listToFilter.length; i++) { |         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++) { |             for (let j = 0; j < listToFilter.length; j++) { | ||||||
|                 if (i === j) { |                 if (i === j) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|                 const guard = listToFilter[j]; |                 const guard = listToFilter[j] | ||||||
|                 if (guard.shadows(tag)) { |                 if (guard.shadows(tag)) { | ||||||
|                     // the guard 'kills' the tag: we continue the outer loop without adding the tag
 |                     // the guard 'kills' the tag: we continue the outer loop without adding the tag
 | ||||||
|                     continue outer; |                     continue outer | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             result.push(tag) |             result.push(tag) | ||||||
|  | @ -615,10 +628,9 @@ export class TagUtils { | ||||||
|      * TagUtils.containsEquivalents([new Tag("key","value")],  [ new Tag("key","other_value")]) // => false
 |      * TagUtils.containsEquivalents([new Tag("key","value")],  [ new Tag("key","other_value")]) // => false
 | ||||||
|      */ |      */ | ||||||
|     public static containsEquivalents(guards: TagsFilter[], listToFilter: TagsFilter[]): boolean { |     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 |      * Parses a level specifier to the various available levels | ||||||
|      * |      * | ||||||
|  | @ -633,24 +645,24 @@ export class TagUtils { | ||||||
|      */ |      */ | ||||||
|     public static LevelsParser(level: string): string[] { |     public static LevelsParser(level: string): string[] { | ||||||
|         let spec = Utils.NoNull([level]) |         let spec = Utils.NoNull([level]) | ||||||
|         spec = [].concat(...spec.map(s => s?.split(";"))) |         spec = [].concat(...spec.map((s) => s?.split(";"))) | ||||||
|         spec = [].concat(...spec.map(s => { |         spec = [].concat( | ||||||
|             s = s.trim() |             ...spec.map((s) => { | ||||||
|             if (s.indexOf("-") < 0 || s.startsWith("-")) { |                 s = s.trim() | ||||||
|                 return s |                 if (s.indexOf("-") < 0 || s.startsWith("-")) { | ||||||
|             } |                     return s | ||||||
|             const [start, end] = s.split("-").map(s => Number(s.trim())) |                 } | ||||||
|             if (isNaN(start) || isNaN(end)) { |                 const [start, end] = s.split("-").map((s) => Number(s.trim())) | ||||||
|                 return undefined |                 if (isNaN(start) || isNaN(end)) { | ||||||
|             } |                     return undefined | ||||||
|             const values = [] |                 } | ||||||
|             for (let i = start; i <= end; i++) { |                 const values = [] | ||||||
|                 values.push(i + "") |                 for (let i = start; i <= end; i++) { | ||||||
|             } |                     values.push(i + "") | ||||||
|             return values |                 } | ||||||
|         })) |                 return values | ||||||
|         return Utils.NoNull(spec); |             }) | ||||||
|  |         ) | ||||||
|  |         return Utils.NoNull(spec) | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,26 +1,25 @@ | ||||||
| export abstract class TagsFilter { | export abstract class TagsFilter { | ||||||
| 
 |  | ||||||
|     abstract asOverpass(): string[] |     abstract asOverpass(): string[] | ||||||
| 
 | 
 | ||||||
|     abstract isUsableAsAnswer(): boolean; |     abstract isUsableAsAnswer(): boolean | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Indicates some form of equivalency: |      * Indicates some form of equivalency: | ||||||
|      * if `this.shadows(t)`, then `this.matches(properties)` implies that `t.matches(properties)` for all possible properties |      * 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 |      * Returns all normal key/value pairs | ||||||
|      * Regex tags, substitutions, comparisons, ... are exempt |      * 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. |      * 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 |      * 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 |      * 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). |      * 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 |      * 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 |  * Various static utils | ||||||
|  */ |  */ | ||||||
| export class Stores { | export class Stores { | ||||||
|     public static Chronic(millis: number, asLong: () => boolean = undefined): Store<Date> { |     public static Chronic(millis: number, asLong: () => boolean = undefined): Store<Date> { | ||||||
|         const source = new UIEventSource<Date>(undefined); |         const source = new UIEventSource<Date>(undefined) | ||||||
| 
 | 
 | ||||||
|         function run() { |         function run() { | ||||||
|             source.setData(new Date()); |             source.setData(new Date()) | ||||||
|             if (asLong === undefined || asLong()) { |             if (asLong === undefined || asLong()) { | ||||||
|                 window.setTimeout(run, millis); |                 window.setTimeout(run, millis) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         run(); |         run() | ||||||
|         return source; |         return source | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static FromPromiseWithErr<T>(promise: Promise<T>): Store<{ success: T } | { error: any }> { |     public static FromPromiseWithErr<T>( | ||||||
|         return UIEventSource.FromPromiseWithErr(promise); |         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> { |     public static FromPromise<T>(promise: Promise<T>): Store<T> { | ||||||
|         const src = new UIEventSource<T>(undefined) |         const src = new UIEventSource<T>(undefined) | ||||||
|         promise?.then(d => src.setData(d)) |         promise?.then((d) => src.setData(d)) | ||||||
|         promise?.catch(err => console.warn("Promise failed:", err)) |         promise?.catch((err) => console.warn("Promise failed:", err)) | ||||||
|         return src |         return src | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static flatten<X>(source: Store<Store<X>>, possibleSources?: Store<any>[]): Store<X> { |     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[]> { |     public static ListStabilized<T>(src: Store<T[]>): Store<T[]> { | ||||||
|         const stable = new UIEventSource<T[]>(undefined) |         const stable = new UIEventSource<T[]>(undefined) | ||||||
|         src.addCallbackAndRun(list => { |         src.addCallbackAndRun((list) => { | ||||||
|             if (list === undefined) { |             if (list === undefined) { | ||||||
|                 stable.setData(undefined) |                 stable.setData(undefined) | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             const oldList = stable.data |             const oldList = stable.data | ||||||
|             if (oldList === list) { |             if (oldList === list) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             if(oldList == list){ |             if (oldList == list) { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             if (oldList === undefined || oldList.length !== list.length) { |             if (oldList === undefined || oldList.length !== list.length) { | ||||||
|                 stable.setData(list); |                 stable.setData(list) | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             for (let i = 0; i < list.length; i++) { |             for (let i = 0; i < list.length; i++) { | ||||||
|                 if (oldList[i] !== list[i]) { |                 if (oldList[i] !== list[i]) { | ||||||
|                     stable.setData(list); |                     stable.setData(list) | ||||||
|                     return; |                     return | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // No actual changes, so we don't do anything
 |             // No actual changes, so we don't do anything
 | ||||||
|             return; |             return | ||||||
|         }) |         }) | ||||||
|         return stable |         return stable | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export abstract class Store<T> { | 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 |      * 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) { |     constructor(tag: string = undefined) { | ||||||
|         this.tag = tag; |         this.tag = tag | ||||||
|         if ((tag === undefined || tag === "")) { |         if (tag === undefined || tag === "") { | ||||||
|             let createStack = Utils.runningFromConsole; |             let createStack = Utils.runningFromConsole | ||||||
|             if (!Utils.runningFromConsole) { |             if (!Utils.runningFromConsole) { | ||||||
|                 createStack = window.location.hostname === "127.0.0.1" |                 createStack = window.location.hostname === "127.0.0.1" | ||||||
|             } |             } | ||||||
|  | @ -109,49 +110,51 @@ export abstract class Store<T> { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     abstract map<J>(f: ((t: T) => J)): Store<J> |     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, extraStoresToWatch: Store<any>[]): Store<J> | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Add a callback function which will run on future data changes |      * 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. |      * Adds a callback function, which will be run immediately. | ||||||
|      * Only triggers if the current data is defined |      * 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 |      * Add a callback function which will run on future data changes | ||||||
|      * Only triggers if the data is defined |      * 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. |      * Adds a callback function, which will be run immediately. | ||||||
|      * Only triggers if the current data is defined |      * 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> { |     public withEqualityStabilized( | ||||||
|         let oldValue = undefined; |         comparator: (t: T | undefined, t1: T | undefined) => boolean | ||||||
|         return this.map(v => { |     ): Store<T> { | ||||||
|  |         let oldValue = undefined | ||||||
|  |         return this.map((v) => { | ||||||
|             if (v == oldValue) { |             if (v == oldValue) { | ||||||
|                 return oldValue |                 return oldValue | ||||||
|             } |             } | ||||||
|             if (comparator(oldValue, v)) { |             if (comparator(oldValue, v)) { | ||||||
|                 return oldValue |                 return oldValue | ||||||
|             } |             } | ||||||
|             oldValue = v; |             oldValue = v | ||||||
|             return v; |             return v | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Monadic bind function |      * Monadic bind function | ||||||
|      *  |      * | ||||||
|      * // simple test with bound and immutablestores
 |      * // simple test with bound and immutablestores
 | ||||||
|      * const src = new UIEventSource<number>(3) |      * const src = new UIEventSource<number>(3) | ||||||
|      * const bound = src.bind(i => new ImmutableStore(i * 2)) |      * const bound = src.bind(i => new ImmutableStore(i * 2)) | ||||||
|  | @ -160,7 +163,7 @@ export abstract class Store<T> { | ||||||
|      * lastValue // => 6
 |      * lastValue // => 6
 | ||||||
|      * src.setData(21) |      * src.setData(21) | ||||||
|      * lastValue // => 42
 |      * lastValue // => 42
 | ||||||
|      *  |      * | ||||||
|      * // simple test with bind over a mapped value
 |      * // simple test with bind over a mapped value
 | ||||||
|      * const src = new UIEventSource<number>(0) |      * const src = new UIEventSource<number>(0) | ||||||
|      * const srcs : UIEventSource<string>[] = [new UIEventSource<string>("a"), new UIEventSource<string>("b")] |      * const srcs : UIEventSource<string>[] = [new UIEventSource<string>("a"), new UIEventSource<string>("b")] | ||||||
|  | @ -176,9 +179,9 @@ export abstract class Store<T> { | ||||||
|      * lastValue // => "xyz"
 |      * lastValue // => "xyz"
 | ||||||
|      * src.setData(0) |      * src.setData(0) | ||||||
|      * lastValue // => "def"
 |      * lastValue // => "def"
 | ||||||
|      *  |      * | ||||||
|      *  |      * | ||||||
|      *  |      * | ||||||
|      * // advanced test with bound
 |      * // advanced test with bound
 | ||||||
|      * const src = new UIEventSource<number>(0) |      * const src = new UIEventSource<number>(0) | ||||||
|      * const srcs : UIEventSource<string>[] = [new UIEventSource<string>("a"), new UIEventSource<string>("b")] |      * const srcs : UIEventSource<string>[] = [new UIEventSource<string>("a"), new UIEventSource<string>("b")] | ||||||
|  | @ -195,20 +198,20 @@ export abstract class Store<T> { | ||||||
|      * src.setData(0) |      * src.setData(0) | ||||||
|      * lastValue // => "def"
 |      * 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 mapped = this.map(f) | ||||||
|         const sink = new UIEventSource<X>(undefined) |         const sink = new UIEventSource<X>(undefined) | ||||||
|         const seenEventSources = new Set<Store<X>>(); |         const seenEventSources = new Set<Store<X>>() | ||||||
|         mapped.addCallbackAndRun(newEventSource => { |         mapped.addCallbackAndRun((newEventSource) => { | ||||||
|             if (newEventSource === null) { |             if (newEventSource === null) { | ||||||
|                 sink.setData(null) |                 sink.setData(null) | ||||||
|             } else if (newEventSource === undefined) { |             } else if (newEventSource === undefined) { | ||||||
|                 sink.setData(undefined) |                 sink.setData(undefined) | ||||||
|             } else if (!seenEventSources.has(newEventSource)) { |             } else if (!seenEventSources.has(newEventSource)) { | ||||||
|                 seenEventSources.add(newEventSource) |                 seenEventSources.add(newEventSource) | ||||||
|                 newEventSource.addCallbackAndRun(resultData => { |                 newEventSource.addCallbackAndRun((resultData) => { | ||||||
|                     if (mapped.data === newEventSource) { |                     if (mapped.data === newEventSource) { | ||||||
|                         sink.setData(resultData); |                         sink.setData(resultData) | ||||||
|                     } |                     } | ||||||
|                 }) |                 }) | ||||||
|             } else { |             } else { | ||||||
|  | @ -217,67 +220,66 @@ export abstract class Store<T> { | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         return sink; |         return sink | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public stabilized(millisToStabilize): Store<T> { |     public stabilized(millisToStabilize): Store<T> { | ||||||
|         if (Utils.runningFromConsole) { |         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(() => { |             window.setTimeout(() => { | ||||||
|                 if (this.data == latestData) { // compare by reference
 |                 if (this.data == latestData) { | ||||||
|                     newSource.setData(latestData); |                     // compare by reference
 | ||||||
|  |                     newSource.setData(latestData) | ||||||
|                 } |                 } | ||||||
|             }, millisToStabilize) |             }, millisToStabilize) | ||||||
|         }); |         }) | ||||||
| 
 | 
 | ||||||
|         return newSource; |         return newSource | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public AsPromise(condition?: ((t: T) => boolean)): Promise<T> { |     public AsPromise(condition?: (t: T) => boolean): Promise<T> { | ||||||
|         const self = this; |         const self = this | ||||||
|         condition = condition ?? (t => t !== undefined) |         condition = condition ?? ((t) => t !== undefined) | ||||||
|         return new Promise((resolve) => { |         return new Promise((resolve) => { | ||||||
|             if (condition(self.data)) { |             if (condition(self.data)) { | ||||||
|                 resolve(self.data) |                 resolve(self.data) | ||||||
|             } else { |             } else { | ||||||
|                 self.addCallbackD(data => { |                 self.addCallbackD((data) => { | ||||||
|                     resolve(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> { | 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) { |     constructor(data: T) { | ||||||
|         super(); |         super() | ||||||
|         this.data = data; |         this.data = data | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addCallback(callback: (data: T) => void): (() => void) { |     addCallback(callback: (data: T) => void): () => void { | ||||||
|         // pass: data will never change
 |         // pass: data will never change
 | ||||||
|         return ImmutableStore.pass |         return ImmutableStore.pass | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addCallbackAndRun(callback: (data: T) => void): (() => void) { |     addCallbackAndRun(callback: (data: T) => void): () => void { | ||||||
|         callback(this.data) |         callback(this.data) | ||||||
|         // no callback registry: data will never change
 |         // no callback registry: data will never change
 | ||||||
|         return ImmutableStore.pass |         return ImmutableStore.pass | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addCallbackAndRunD(callback: (data: T) => void): (() => void) { |     addCallbackAndRunD(callback: (data: T) => void): () => void { | ||||||
|         if (this.data !== undefined) { |         if (this.data !== undefined) { | ||||||
|             callback(this.data) |             callback(this.data) | ||||||
|         } |         } | ||||||
|  | @ -285,38 +287,35 @@ export class ImmutableStore<T> extends Store<T> { | ||||||
|         return ImmutableStore.pass |         return ImmutableStore.pass | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addCallbackD(callback: (data: T) => void): (() => void) { |     addCallbackD(callback: (data: T) => void): () => void { | ||||||
|         // pass: data will never change
 |         // pass: data will never change
 | ||||||
|         return ImmutableStore.pass |         return ImmutableStore.pass | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     map<J>(f: (t: T) => J, extraStores: Store<any>[] = undefined): ImmutableStore<J> { |     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 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 |  * Keeps track of the callback functions | ||||||
|  */ |  */ | ||||||
| class ListenerTracker<T> { | 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 |      * 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) { |         if (callback === console.log) { | ||||||
|             // This ^^^ actually works!
 |             // 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." |             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!
 |         // Give back an unregister-function!
 | ||||||
|         return () => { |         return () => { | ||||||
|  | @ -332,9 +331,9 @@ class ListenerTracker<T> { | ||||||
|      * Returns the number of registered callbacks |      * Returns the number of registered callbacks | ||||||
|      */ |      */ | ||||||
|     public ping(data: T): number { |     public ping(data: T): number { | ||||||
|         this.pingCount ++; |         this.pingCount++ | ||||||
|         let toDelete = undefined |         let toDelete = undefined | ||||||
|         let startTime = new Date().getTime() / 1000; |         let startTime = new Date().getTime() / 1000 | ||||||
|         for (const callback of this._callbacks) { |         for (const callback of this._callbacks) { | ||||||
|             if (callback(data) === true) { |             if (callback(data) === true) { | ||||||
|                 // This callback wants to be deleted
 |                 // This callback wants to be deleted
 | ||||||
|  | @ -347,8 +346,10 @@ class ListenerTracker<T> { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         let endTime = new Date().getTime() / 1000 |         let endTime = new Date().getTime() / 1000 | ||||||
|         if ((endTime - startTime) > 500) { |         if (endTime - startTime > 500) { | ||||||
|             console.trace("Warning: a ping took more then 500ms; this is probably a performance issue") |             console.trace( | ||||||
|  |                 "Warning: a ping took more then 500ms; this is probably a performance issue" | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|         if (toDelete !== undefined) { |         if (toDelete !== undefined) { | ||||||
|             for (const toDeleteElement of toDelete) { |             for (const toDeleteElement of toDelete) { | ||||||
|  | @ -363,55 +364,57 @@ class ListenerTracker<T> { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  * The mapped store is a helper type which does the mapping of a function. |  * The mapped store is a helper type which does the mapping of a function. | ||||||
|  * It'll fuse |  * It'll fuse | ||||||
|  */ |  */ | ||||||
| class MappedStore<TIn, T> extends Store<T> { | 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 _f: (t: TIn) => T | ||||||
|     private _upstreamCallbackHandler: ListenerTracker<TIn> | undefined; |     private readonly _extraStores: Store<any>[] | undefined | ||||||
|     private _upstreamPingCount: number = -1; |  | ||||||
|     private _unregisterFromUpstream: (() => void) |  | ||||||
|      |  | ||||||
|     private _f: (t: TIn) => T; |  | ||||||
|     private readonly _extraStores: Store<any>[] | undefined; |  | ||||||
|     private _unregisterFromExtraStores: (() => void)[] | undefined |     private _unregisterFromExtraStores: (() => void)[] | undefined | ||||||
| 
 | 
 | ||||||
|     private _callbacks: ListenerTracker<T> = new ListenerTracker<T>() |     private _callbacks: ListenerTracker<T> = new ListenerTracker<T>() | ||||||
| 
 | 
 | ||||||
|     private static readonly pass: () => {} |     private static readonly pass: () => {} | ||||||
| 
 | 
 | ||||||
| 
 |     constructor( | ||||||
|     constructor(upstream: Store<TIn>, f: (t: TIn) => T, extraStores: Store<any>[],  |         upstream: Store<TIn>, | ||||||
|                 upstreamListenerHandler: ListenerTracker<TIn> | undefined, initialState: T) { |         f: (t: TIn) => T, | ||||||
|         super(); |         extraStores: Store<any>[], | ||||||
|         this._upstream = upstream; |         upstreamListenerHandler: ListenerTracker<TIn> | undefined, | ||||||
|  |         initialState: T | ||||||
|  |     ) { | ||||||
|  |         super() | ||||||
|  |         this._upstream = upstream | ||||||
|         this._upstreamCallbackHandler = upstreamListenerHandler |         this._upstreamCallbackHandler = upstreamListenerHandler | ||||||
|         this._f = f; |         this._f = f | ||||||
|         this._data = initialState |         this._data = initialState | ||||||
|         this._upstreamPingCount = upstreamListenerHandler?.pingCount |         this._upstreamPingCount = upstreamListenerHandler?.pingCount | ||||||
|         this._extraStores = extraStores; |         this._extraStores = extraStores | ||||||
|         this.registerCallbacksToUpstream() |         this.registerCallbacksToUpstream() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private _data: T; |     private _data: T | ||||||
|     private _callbacksAreRegistered = false |     private _callbacksAreRegistered = false | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Gets the current data from the store |      * Gets the current data from the store | ||||||
|      *  |      * | ||||||
|      * const src = new UIEventSource(21) |      * const src = new UIEventSource(21) | ||||||
|      * const mapped = src.map(i => i * 2) |      * const mapped = src.map(i => i * 2) | ||||||
|      * src.setData(3) |      * src.setData(3) | ||||||
|      * mapped.data // => 6
 |      * mapped.data // => 6
 | ||||||
|      *  |      * | ||||||
|      */ |      */ | ||||||
|     get data(): T {  |     get data(): T { | ||||||
|         if (!this._callbacksAreRegistered) { |         if (!this._callbacksAreRegistered) { | ||||||
|             // Callbacks are not registered, so we haven't been listening for updates from the upstream which might have changed
 |             // 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
 |                 // Upstream has pinged - let's update our data first
 | ||||||
|                 this._data = this._f(this._upstream.data) |                 this._data = this._f(this._upstream.data) | ||||||
|             } |             } | ||||||
|  | @ -420,8 +423,7 @@ class MappedStore<TIn, T> extends Store<T> { | ||||||
|         return this._data |         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 |         let stores: Store<any>[] = undefined | ||||||
|         if (extraStores?.length > 0 || this._extraStores?.length > 0) { |         if (extraStores?.length > 0 || this._extraStores?.length > 0) { | ||||||
|             stores = [] |             stores = [] | ||||||
|  | @ -430,7 +432,7 @@ class MappedStore<TIn, T> extends Store<T> { | ||||||
|             stores.push(...extraStores) |             stores.push(...extraStores) | ||||||
|         } |         } | ||||||
|         if (this._extraStores?.length > 0) { |         if (this._extraStores?.length > 0) { | ||||||
|             this._extraStores?.forEach(store => { |             this._extraStores?.forEach((store) => { | ||||||
|                 if (stores.indexOf(store) < 0) { |                 if (stores.indexOf(store) < 0) { | ||||||
|                     stores.push(store) |                     stores.push(store) | ||||||
|                 } |                 } | ||||||
|  | @ -442,39 +444,37 @@ class MappedStore<TIn, T> extends Store<T> { | ||||||
|             stores, |             stores, | ||||||
|             this._callbacks, |             this._callbacks, | ||||||
|             f(this.data) |             f(this.data) | ||||||
|         ); |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private unregisterFromUpstream() { |     private unregisterFromUpstream() { | ||||||
|         console.log("Unregistering callbacks for", this.tag) |         console.log("Unregistering callbacks for", this.tag) | ||||||
|         this._callbacksAreRegistered = false; |         this._callbacksAreRegistered = false | ||||||
|         this._unregisterFromUpstream() |         this._unregisterFromUpstream() | ||||||
|         this._unregisterFromExtraStores?.forEach(unr => unr()) |         this._unregisterFromExtraStores?.forEach((unr) => unr()) | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     private registerCallbacksToUpstream() { |     private registerCallbacksToUpstream() { | ||||||
|         const self = this |         const self = this | ||||||
|         | 
 | ||||||
|         this._unregisterFromUpstream = this._upstream.addCallback( |         this._unregisterFromUpstream = this._upstream.addCallback((_) => self.update()) | ||||||
|             _ => self.update() |         this._unregisterFromExtraStores = this._extraStores?.map((store) => | ||||||
|  |             store?.addCallback((_) => self.update()) | ||||||
|         ) |         ) | ||||||
|         this._unregisterFromExtraStores = this._extraStores?.map(store => |         this._callbacksAreRegistered = true | ||||||
|             store?.addCallback(_ => self.update()) |  | ||||||
|         ) |  | ||||||
|         this._callbacksAreRegistered = true; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private update(): void { |     private update(): void { | ||||||
|         const newData = this._f(this._upstream.data) |         const newData = this._f(this._upstream.data) | ||||||
|         this._upstreamPingCount = this._upstreamCallbackHandler?.pingCount |         this._upstreamPingCount = this._upstreamCallbackHandler?.pingCount | ||||||
|         if (this._data == newData) { |         if (this._data == newData) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         this._data = newData |         this._data = newData | ||||||
|         this._callbacks.ping(this._data) |         this._callbacks.ping(this._data) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addCallback(callback: (data: T) => (any | boolean | void)): (() => void) { |     addCallback(callback: (data: T) => any | boolean | void): () => void { | ||||||
|         if (!this._callbacksAreRegistered) { |         if (!this._callbacksAreRegistered) { | ||||||
|             // This is the first callback that is added
 |             // This is the first callback that is added
 | ||||||
|             // We register this 'map' to the upstream object and all the streams
 |             // 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 unregister = this.addCallback(callback) | ||||||
|         const doRemove = callback(this.data) |         const doRemove = callback(this.data) | ||||||
|         if (doRemove === true) { |         if (doRemove === true) { | ||||||
|  | @ -499,71 +499,74 @@ class MappedStore<TIn, T> extends Store<T> { | ||||||
|         return unregister |         return unregister | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addCallbackAndRunD(callback: (data: T) => (any | boolean | void)): (() => void) { |     addCallbackAndRunD(callback: (data: T) => any | boolean | void): () => void { | ||||||
|         return this.addCallbackAndRun(data => { |         return this.addCallbackAndRun((data) => { | ||||||
|             if (data !== undefined) { |             if (data !== undefined) { | ||||||
|                 return callback(data) |                 return callback(data) | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addCallbackD(callback: (data: T) => (any | boolean | void)): (() => void) { |     addCallbackD(callback: (data: T) => any | boolean | void): () => void { | ||||||
|         return this.addCallback(data => { |         return this.addCallback((data) => { | ||||||
|             if (data !== undefined) { |             if (data !== undefined) { | ||||||
|                 return callback(data) |                 return callback(data) | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class UIEventSource<T> extends Store<T> { | export class UIEventSource<T> extends Store<T> { | ||||||
| 
 |     public data: T | ||||||
|     public data: T; |  | ||||||
|     _callbacks: ListenerTracker<T> = new ListenerTracker<T>() |     _callbacks: ListenerTracker<T> = new ListenerTracker<T>() | ||||||
| 
 | 
 | ||||||
|     private static readonly pass: () => {} |     private static readonly pass: () => {} | ||||||
| 
 | 
 | ||||||
|     constructor(data: T, tag: string = "") { |     constructor(data: T, tag: string = "") { | ||||||
|         super(tag); |         super(tag) | ||||||
|         this.data = data; |         this.data = data | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static flatten<X>(source: Store<Store<X>>, possibleSources?: Store<any>[]): UIEventSource<X> { |     public static flatten<X>( | ||||||
|         const sink = new UIEventSource<X>(source.data?.data); |         source: Store<Store<X>>, | ||||||
|  |         possibleSources?: Store<any>[] | ||||||
|  |     ): UIEventSource<X> { | ||||||
|  |         const sink = new UIEventSource<X>(source.data?.data) | ||||||
| 
 | 
 | ||||||
|         source.addCallback((latestData) => { |         source.addCallback((latestData) => { | ||||||
|             sink.setData(latestData?.data); |             sink.setData(latestData?.data) | ||||||
|             latestData.addCallback(data => { |             latestData.addCallback((data) => { | ||||||
|                 if (source.data !== latestData) { |                 if (source.data !== latestData) { | ||||||
|                     return true; |                     return true | ||||||
|                 } |                 } | ||||||
|                 sink.setData(data) |                 sink.setData(data) | ||||||
|             }) |             }) | ||||||
|         }); |         }) | ||||||
| 
 | 
 | ||||||
|         for (const possibleSource of possibleSources ?? []) { |         for (const possibleSource of possibleSources ?? []) { | ||||||
|             possibleSource?.addCallback(() => { |             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. |      * 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 |      * 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) |         const src = new UIEventSource<T>(undefined) | ||||||
|         promise?.then(d => src.setData(d)) |         promise?.then((d) => src.setData(d)) | ||||||
|         promise?.catch(err => { |         promise?.catch((err) => { | ||||||
|             if (onError !== undefined) { |             if (onError !== undefined) { | ||||||
|                 onError(err) |                 onError(err) | ||||||
|             } else { |             } else { | ||||||
|                 console.warn("Promise failed:", err); |                 console.warn("Promise failed:", err) | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|         return src |         return src | ||||||
|  | @ -575,25 +578,27 @@ export class UIEventSource<T> extends Store<T> { | ||||||
|      * @param promise |      * @param promise | ||||||
|      * @constructor |      * @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) |         const src = new UIEventSource<{ success: T } | { error: any }>(undefined) | ||||||
|         promise?.then(d => src.setData({success: d})) |         promise?.then((d) => src.setData({ success: d })) | ||||||
|         promise?.catch(err => src.setData({error: err})) |         promise?.catch((err) => src.setData({ error: err })) | ||||||
|         return src |         return src | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static asFloat(source: UIEventSource<string>): UIEventSource<number> { |     public static asFloat(source: UIEventSource<string>): UIEventSource<number> { | ||||||
|         return source.sync( |         return source.sync( | ||||||
|             (str) => { |             (str) => { | ||||||
|                 let parsed = parseFloat(str); |                 let parsed = parseFloat(str) | ||||||
|                 return isNaN(parsed) ? undefined : parsed; |                 return isNaN(parsed) ? undefined : parsed | ||||||
|             }, |             }, | ||||||
|             [], |             [], | ||||||
|             (fl) => { |             (fl) => { | ||||||
|                 if (fl === undefined || isNaN(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 |      * If the result of the callback is 'true', the callback is considered finished and will be removed again | ||||||
|      * @param callback |      * @param callback | ||||||
|      */ |      */ | ||||||
|     public addCallback(callback: ((latestData: T) => (boolean | void | any))): (() => void) { |     public addCallback(callback: (latestData: T) => boolean | void | any): () => void { | ||||||
|         return this._callbacks.addCallback(callback); |         return this._callbacks.addCallback(callback) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public addCallbackAndRun(callback: ((latestData: T) => (boolean | void | any))): (() => void) { |     public addCallbackAndRun(callback: (latestData: T) => boolean | void | any): () => void { | ||||||
|         const doDeleteCallback = callback(this.data); |         const doDeleteCallback = callback(this.data) | ||||||
|         if (doDeleteCallback !== true) { |         if (doDeleteCallback !== true) { | ||||||
|             return this.addCallback(callback); |             return this.addCallback(callback) | ||||||
|         } else { |         } else { | ||||||
|             return UIEventSource.pass |             return UIEventSource.pass | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public addCallbackAndRunD(callback: (data: T) => void): (() => void) { |     public addCallbackAndRunD(callback: (data: T) => void): () => void { | ||||||
|         return this.addCallbackAndRun(data => { |         return this.addCallbackAndRun((data) => { | ||||||
|             if (data !== undefined && data !== null) { |             if (data !== undefined && data !== null) { | ||||||
|                 return callback(data) |                 return callback(data) | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public addCallbackD(callback: (data: T) => void): (() => void) { |     public addCallbackD(callback: (data: T) => void): () => void { | ||||||
|         return this.addCallback(data => { |         return this.addCallback((data) => { | ||||||
|             if (data !== undefined && data !== null) { |             if (data !== undefined && data !== null) { | ||||||
|                 return callback(data) |                 return callback(data) | ||||||
|             } |             } | ||||||
|  | @ -634,12 +639,13 @@ export class UIEventSource<T> extends Store<T> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public setData(t: T): UIEventSource<T> { |     public setData(t: T): UIEventSource<T> { | ||||||
|         if (this.data == t) { // MUST COMPARE BY REFERENCE!
 |         if (this.data == t) { | ||||||
|             return; |             // MUST COMPARE BY REFERENCE!
 | ||||||
|  |             return | ||||||
|         } |         } | ||||||
|         this.data = t; |         this.data = t | ||||||
|         this._callbacks.ping(t) |         this._callbacks.ping(t) | ||||||
|         return this; |         return this | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public ping(): void { |     public ping(): void { | ||||||
|  | @ -669,9 +675,8 @@ export class UIEventSource<T> extends Store<T> { | ||||||
|      * srcSeen // => 21
 |      * srcSeen // => 21
 | ||||||
|      * lastSeen // => 42
 |      * lastSeen // => 42
 | ||||||
|      */ |      */ | ||||||
|     public map<J>(f: ((t: T) => J), |     public map<J>(f: (t: T) => J, extraSources: Store<any>[] = []): Store<J> { | ||||||
|                   extraSources: Store<any>[] = []): Store<J> { |         return new MappedStore(this, f, extraSources, this._callbacks, f(this.data)) | ||||||
|         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 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 |      * @param allowUnregister: if set, the update will be halted if no listeners are registered | ||||||
|      */ |      */ | ||||||
|     public sync<J>(f: ((t: T) => J), |     public sync<J>( | ||||||
|                    extraSources: Store<any>[], |         f: (t: T) => J, | ||||||
|                    g: ((j: J, t: T) => T), |         extraSources: Store<any>[], | ||||||
|                    allowUnregister = false): UIEventSource<J> { |         g: (j: J, t: T) => T, | ||||||
|         const self = this; |         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 callee = stack[1] | ||||||
| 
 | 
 | ||||||
|         const newSource = new UIEventSource<J>( |         const newSource = new UIEventSource<J>(f(this.data), "map(" + this.tag + ")@" + callee) | ||||||
|             f(this.data), |  | ||||||
|             "map(" + this.tag + ")@" + callee |  | ||||||
|         ); |  | ||||||
| 
 | 
 | ||||||
|         const update = function () { |         const update = function () { | ||||||
|             newSource.setData(f(self.data)); |             newSource.setData(f(self.data)) | ||||||
|             return allowUnregister && newSource._callbacks.length() === 0 |             return allowUnregister && newSource._callbacks.length() === 0 | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.addCallback(update); |         this.addCallback(update) | ||||||
|         for (const extraSource of extraSources) { |         for (const extraSource of extraSources) { | ||||||
|             extraSource?.addCallback(update); |             extraSource?.addCallback(update) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (g !== undefined) { |         if (g !== undefined) { | ||||||
|             newSource.addCallback((latest) => { |             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> { |     public syncWith(otherSource: UIEventSource<T>, reverseOverride = false): UIEventSource<T> { | ||||||
|         this.addCallback((latest) => otherSource.setData(latest)); |         this.addCallback((latest) => otherSource.setData(latest)) | ||||||
|         const self = this; |         const self = this | ||||||
|         otherSource.addCallback((latest) => self.setData(latest)); |         otherSource.addCallback((latest) => self.setData(latest)) | ||||||
|         if (reverseOverride) { |         if (reverseOverride) { | ||||||
|             if (otherSource.data !== undefined) { |             if (otherSource.data !== undefined) { | ||||||
|                 this.setData(otherSource.data); |                 this.setData(otherSource.data) | ||||||
|             } |             } | ||||||
|         } else if (this.data === undefined) { |         } else if (this.data === undefined) { | ||||||
|             this.setData(otherSource.data); |             this.setData(otherSource.data) | ||||||
|         } else { |         } else { | ||||||
|             otherSource.setData(this.data); |             otherSource.setData(this.data) | ||||||
|         } |         } | ||||||
|         return this; |         return this | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,12 +1,11 @@ | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Wrapper around the hash to create an UIEventSource from it |  * Wrapper around the hash to create an UIEventSource from it | ||||||
|  */ |  */ | ||||||
| export default class Hash { | 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 |      * 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 === "") { |         if (Hash.hash.data === undefined || Hash.hash.data === "") { | ||||||
|             return "" |             return "" | ||||||
|         } else { |         } else { | ||||||
|             return "#" + Hash.hash.data; |             return "#" + Hash.hash.data | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static Get(): UIEventSource<string> { |     private static Get(): UIEventSource<string> { | ||||||
|         if (Utils.runningFromConsole) { |         if (Utils.runningFromConsole) { | ||||||
|             return new UIEventSource<string>(undefined); |             return new UIEventSource<string>(undefined) | ||||||
|         } |         } | ||||||
|         const hash = new UIEventSource<string>(window.location.hash.substr(1)); |         const hash = new UIEventSource<string>(window.location.hash.substr(1)) | ||||||
|         hash.addCallback(h => { |         hash.addCallback((h) => { | ||||||
|             if (h === "undefined") { |             if (h === "undefined") { | ||||||
|                 console.warn("Got a literal 'undefined' as hash, ignoring") |                 console.warn("Got a literal 'undefined' as hash, ignoring") | ||||||
|                 h = undefined; |                 h = undefined | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (h === undefined || h === "") { |             if (h === undefined || h === "") { | ||||||
|                 window.location.hash = ""; |                 window.location.hash = "" | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             history.pushState({}, "") |             history.pushState({}, "") | ||||||
|             window.location.hash = "#" + h; |             window.location.hash = "#" + h | ||||||
|         }); |         }) | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|         window.onhashchange = () => { |         window.onhashchange = () => { | ||||||
|             let newValue = window.location.hash.substr(1); |             let newValue = window.location.hash.substr(1) | ||||||
|             if (newValue === "") { |             if (newValue === "") { | ||||||
|                 newValue = undefined; |                 newValue = undefined | ||||||
|             } |             } | ||||||
|             hash.setData(newValue) |             hash.setData(newValue) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         window.addEventListener('popstate', _ => { |         window.addEventListener("popstate", (_) => { | ||||||
|             let newValue = window.location.hash.substr(1); |             let newValue = window.location.hash.substr(1) | ||||||
|             if (newValue === "") { |             if (newValue === "") { | ||||||
|                 newValue = undefined; |                 newValue = undefined | ||||||
|             } |             } | ||||||
|             hash.setData(newValue) |             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 * as idb from "idb-keyval" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * UIEventsource-wrapper around indexedDB key-value |  * UIEventsource-wrapper around indexedDB key-value | ||||||
|  */ |  */ | ||||||
| export class IdbLocalStorage { | export class IdbLocalStorage { | ||||||
| 
 |  | ||||||
|     private static readonly _sourceCache: Record<string, UIEventSource<any>> = {} |     private static readonly _sourceCache: Record<string, UIEventSource<any>> = {} | ||||||
|      | 
 | ||||||
|     public static Get<T>(key: string, options?: { defaultValue?: T, whenLoaded?: (t: T | null) => void }): UIEventSource<T> { |     public static Get<T>( | ||||||
|         if(IdbLocalStorage._sourceCache[key] !== undefined){ |         key: string, | ||||||
|  |         options?: { defaultValue?: T; whenLoaded?: (t: T | null) => void } | ||||||
|  |     ): UIEventSource<T> { | ||||||
|  |         if (IdbLocalStorage._sourceCache[key] !== undefined) { | ||||||
|             return IdbLocalStorage._sourceCache[key] |             return IdbLocalStorage._sourceCache[key] | ||||||
|         } |         } | ||||||
|         const src = new UIEventSource<T>(options?.defaultValue, "idb-local-storage:" + key) |         const src = new UIEventSource<T>(options?.defaultValue, "idb-local-storage:" + key) | ||||||
|         if (Utils.runningFromConsole) { |         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); |  | ||||||
|             if (options?.whenLoaded !== undefined) { |  | ||||||
|                 options?.whenLoaded(v) |  | ||||||
|             } |  | ||||||
|         }).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; |  | ||||||
| 
 | 
 | ||||||
|  |         idb.get(key) | ||||||
|  |             .then((v) => { | ||||||
|  |                 src.setData(v ?? options?.defaultValue) | ||||||
|  |                 if (options?.whenLoaded !== undefined) { | ||||||
|  |                     options?.whenLoaded(v) | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |             .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 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static SetDirectly(key: string, value) { |     public static SetDirectly(key: string, value) { | ||||||
|  |  | ||||||
|  | @ -1,51 +1,47 @@ | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Fetches data from random data sources, used in the metatagging |  * Fetches data from random data sources, used in the metatagging | ||||||
|  */ |  */ | ||||||
| export default class LiveQueryHandler { | export default class LiveQueryHandler { | ||||||
| 
 |  | ||||||
|     private static neededShorthands = {} // url -> (shorthand:paths)[]
 |     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] ?? [] |         const shorthandsSet: string[] = LiveQueryHandler.neededShorthands[url] ?? [] | ||||||
| 
 | 
 | ||||||
|         for (const shorthand of shorthands) { |         for (const shorthand of shorthands) { | ||||||
|             if (shorthandsSet.indexOf(shorthand) < 0) { |             if (shorthandsSet.indexOf(shorthand) < 0) { | ||||||
|                 shorthandsSet.push(shorthand); |                 shorthandsSet.push(shorthand) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         LiveQueryHandler.neededShorthands[url] = shorthandsSet; |         LiveQueryHandler.neededShorthands[url] = shorthandsSet | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|         if (LiveQueryHandler[url] === undefined) { |         if (LiveQueryHandler[url] === undefined) { | ||||||
|             const source = new UIEventSource({}); |             const source = new UIEventSource({}) | ||||||
|             LiveQueryHandler[url] = source; |             LiveQueryHandler[url] = source | ||||||
| 
 | 
 | ||||||
|             console.log("Fetching live data from a third-party (unknown) API:", url) |             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) { |                 for (const shorthandDescription of shorthandsSet) { | ||||||
| 
 |                     const descr = shorthandDescription.trim().split(":") | ||||||
|                     const descr = shorthandDescription.trim().split(":"); |                     const shorthand = descr[0] | ||||||
|                     const shorthand = descr[0]; |                     const path = descr[1] | ||||||
|                     const path = descr[1]; |                     const parts = path.split(".") | ||||||
|                     const parts = path.split("."); |                     let trail = data | ||||||
|                     let trail = data; |  | ||||||
|                     for (const part of parts) { |                     for (const part of parts) { | ||||||
|                         if (trail !== undefined) { |                         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 |  * UIEventsource-wrapper around localStorage | ||||||
|  */ |  */ | ||||||
| export class LocalStorageSource { | export class LocalStorageSource { | ||||||
| 
 |  | ||||||
|     static GetParsed<T>(key: string, defaultValue: T): UIEventSource<T> { |     static GetParsed<T>(key: string, defaultValue: T): UIEventSource<T> { | ||||||
|         return LocalStorageSource.Get(key).sync( |         return LocalStorageSource.Get(key).sync( | ||||||
|             str => { |             (str) => { | ||||||
|                 if (str === undefined) { |                 if (str === undefined) { | ||||||
|                     return defaultValue |                     return defaultValue | ||||||
|                 } |                 } | ||||||
|  | @ -16,29 +15,29 @@ export class LocalStorageSource { | ||||||
|                 } catch { |                 } catch { | ||||||
|                     return defaultValue |                     return defaultValue | ||||||
|                 } |                 } | ||||||
|             }, [], |             }, | ||||||
|             value => JSON.stringify(value) |             [], | ||||||
|  |             (value) => JSON.stringify(value) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static Get(key: string, defaultValue: string = undefined): UIEventSource<string> { |     static Get(key: string, defaultValue: string = undefined): UIEventSource<string> { | ||||||
|         try { |         try { | ||||||
|             const saved = localStorage.getItem(key); |             const saved = localStorage.getItem(key) | ||||||
|             const source = new UIEventSource<string>(saved ?? defaultValue, "localstorage:" + key); |             const source = new UIEventSource<string>(saved ?? defaultValue, "localstorage:" + key) | ||||||
| 
 | 
 | ||||||
|             source.addCallback((data) => { |             source.addCallback((data) => { | ||||||
|                 try { |                 try { | ||||||
|                     localStorage.setItem(key, data); |                     localStorage.setItem(key, data) | ||||||
|                 } catch (e) { |                 } catch (e) { | ||||||
|                     // Probably exceeded the quota with this item!
 |                     // Probably exceeded the quota with this item!
 | ||||||
|                     // Lets nuke everything
 |                     // Lets nuke everything
 | ||||||
|                     localStorage.clear() |                     localStorage.clear() | ||||||
|                 } |                 } | ||||||
| 
 |             }) | ||||||
|             }); |             return source | ||||||
|             return source; |  | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             return new UIEventSource<string>(defaultValue); |             return new UIEventSource<string>(defaultValue) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,31 +1,31 @@ | ||||||
| import * as mangrove from 'mangrove-reviews' | import * as mangrove from "mangrove-reviews" | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import {Review} from "./Review"; | import { Review } from "./Review" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| export class MangroveIdentity { | export class MangroveIdentity { | ||||||
|     public keypair: any = undefined; |     public keypair: any = undefined | ||||||
|     public readonly kid: UIEventSource<string> = new UIEventSource<string>(undefined); |     public readonly kid: UIEventSource<string> = new UIEventSource<string>(undefined) | ||||||
|     private readonly _mangroveIdentity: UIEventSource<string>; |     private readonly _mangroveIdentity: UIEventSource<string> | ||||||
| 
 | 
 | ||||||
|     constructor(mangroveIdentity: UIEventSource<string>) { |     constructor(mangroveIdentity: UIEventSource<string>) { | ||||||
|         const self = this; |         const self = this | ||||||
|         this._mangroveIdentity = mangroveIdentity; |         this._mangroveIdentity = mangroveIdentity | ||||||
|         mangroveIdentity.addCallbackAndRunD(str => { |         mangroveIdentity.addCallbackAndRunD((str) => { | ||||||
|             if (str === "") { |             if (str === "") { | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             mangrove.jwkToKeypair(JSON.parse(str)).then(keypair => { |             mangrove.jwkToKeypair(JSON.parse(str)).then((keypair) => { | ||||||
|                 self.keypair = keypair; |                 self.keypair = keypair | ||||||
|                 mangrove.publicToPem(keypair.publicKey).then(pem => { |                 mangrove.publicToPem(keypair.publicKey).then((pem) => { | ||||||
|                     console.log("Identity loaded") |                     console.log("Identity loaded") | ||||||
|                     self.kid.setData(pem); |                     self.kid.setData(pem) | ||||||
|                 }) |                 }) | ||||||
|             }) |             }) | ||||||
|         }) |         }) | ||||||
|         try { |         try { | ||||||
|             if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") { |             if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") { | ||||||
|                 this.CreateIdentity(); |                 this.CreateIdentity() | ||||||
|             } |             } | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.error("Could not create identity: ", e) |             console.error("Could not create identity: ", e) | ||||||
|  | @ -41,58 +41,62 @@ export class MangroveIdentity { | ||||||
|         if ("" !== (this._mangroveIdentity.data ?? "")) { |         if ("" !== (this._mangroveIdentity.data ?? "")) { | ||||||
|             throw "Identity already defined - not creating a new one" |             throw "Identity already defined - not creating a new one" | ||||||
|         } |         } | ||||||
|         const self = this; |         const self = this | ||||||
|         mangrove.generateKeypair().then( |         mangrove.generateKeypair().then((keypair) => { | ||||||
|             keypair => { |             self.keypair = keypair | ||||||
|                 self.keypair = keypair; |             mangrove.keypairToJwk(keypair).then((jwk) => { | ||||||
|                 mangrove.keypairToJwk(keypair).then(jwk => { |                 self._mangroveIdentity.setData(JSON.stringify(jwk)) | ||||||
|                     self._mangroveIdentity.setData(JSON.stringify(jwk)); |             }) | ||||||
|                 }) |         }) | ||||||
|             }); |  | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default class MangroveReviews { | export default class MangroveReviews { | ||||||
|     private static _reviewsCache = {}; |     private static _reviewsCache = {} | ||||||
|     private static didWarn = false; |     private static didWarn = false | ||||||
|     private readonly _lon: number; |     private readonly _lon: number | ||||||
|     private readonly _lat: number; |     private readonly _lat: number | ||||||
|     private readonly _name: string; |     private readonly _name: string | ||||||
|     private readonly _reviews: UIEventSource<Review[]> = new UIEventSource<Review[]>([]); |     private readonly _reviews: UIEventSource<Review[]> = new UIEventSource<Review[]>([]) | ||||||
|     private _dryRun: boolean; |     private _dryRun: boolean | ||||||
|     private _mangroveIdentity: MangroveIdentity; |     private _mangroveIdentity: MangroveIdentity | ||||||
|     private _lastUpdate: Date = undefined; |     private _lastUpdate: Date = undefined | ||||||
| 
 | 
 | ||||||
|     private constructor(lon: number, lat: number, name: string, |     private constructor( | ||||||
|                         identity: MangroveIdentity, |         lon: number, | ||||||
|                         dryRun?: boolean) { |         lat: number, | ||||||
| 
 |         name: string, | ||||||
|         this._lon = lon; |         identity: MangroveIdentity, | ||||||
|         this._lat = lat; |         dryRun?: boolean | ||||||
|         this._name = name; |     ) { | ||||||
|         this._mangroveIdentity = identity; |         this._lon = lon | ||||||
|         this._dryRun = dryRun; |         this._lat = lat | ||||||
|  |         this._name = name | ||||||
|  |         this._mangroveIdentity = identity | ||||||
|  |         this._dryRun = dryRun | ||||||
|         if (dryRun && !MangroveReviews.didWarn) { |         if (dryRun && !MangroveReviews.didWarn) { | ||||||
|             MangroveReviews.didWarn = true; |             MangroveReviews.didWarn = true | ||||||
|             console.warn("Mangrove reviews will _not_ be saved as dryrun is specified") |             console.warn("Mangrove reviews will _not_ be saved as dryrun is specified") | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static Get(lon: number, lat: number, name: string, |     public static Get( | ||||||
|                       identity: MangroveIdentity, |         lon: number, | ||||||
|                       dryRun?: boolean) { |         lat: number, | ||||||
|         const newReviews = new MangroveReviews(lon, lat, name, identity, dryRun); |         name: string, | ||||||
|  |         identity: MangroveIdentity, | ||||||
|  |         dryRun?: boolean | ||||||
|  |     ) { | ||||||
|  |         const newReviews = new MangroveReviews(lon, lat, name, identity, dryRun) | ||||||
| 
 | 
 | ||||||
|         const uri = newReviews.GetSubjectUri(); |         const uri = newReviews.GetSubjectUri() | ||||||
|         const cached = MangroveReviews._reviewsCache[uri]; |         const cached = MangroveReviews._reviewsCache[uri] | ||||||
|         if (cached !== undefined) { |         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 |      * @constructor | ||||||
|      */ |      */ | ||||||
|     public GetSubjectUri() { |     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) { |         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. |      * Gives a UIEVentsource with all reviews. | ||||||
|      * Note: rating is between 1 and 100 |      * Note: rating is between 1 and 100 | ||||||
|      */ |      */ | ||||||
|     public GetReviews(): UIEventSource<Review[]> { |     public GetReviews(): UIEventSource<Review[]> { | ||||||
| 
 |         if ( | ||||||
|         if (this._lastUpdate !== undefined && this._reviews.data !== undefined && |             this._lastUpdate !== undefined && | ||||||
|             (new Date().getTime() - this._lastUpdate.getTime()) < 15000 |             this._reviews.data !== undefined && | ||||||
|  |             new Date().getTime() - this._lastUpdate.getTime() < 15000 | ||||||
|         ) { |         ) { | ||||||
|             // Last update was pretty recent
 |             // Last update was pretty recent
 | ||||||
|             return this._reviews; |             return this._reviews | ||||||
|         } |         } | ||||||
|         this._lastUpdate = new Date(); |         this._lastUpdate = new Date() | ||||||
| 
 | 
 | ||||||
|         const self = this; |         const self = this | ||||||
|         mangrove.getReviews({sub: this.GetSubjectUri()}).then( |         mangrove.getReviews({ sub: this.GetSubjectUri() }).then((data) => { | ||||||
|             (data) => { |             const reviews = [] | ||||||
|                 const reviews = []; |             const reviewsByUser = [] | ||||||
|                 const reviewsByUser = []; |             for (const review of data.reviews) { | ||||||
|                 for (const review of data.reviews) { |                 const r = review.payload | ||||||
|                     const r = review.payload; |  | ||||||
| 
 | 
 | ||||||
| 
 |                 console.log( | ||||||
|                     console.log("PublicKey is ", self._mangroveIdentity.kid.data, "reviews.kid is", review.kid); |                     "PublicKey is ", | ||||||
|                     const byUser = self._mangroveIdentity.kid.map(data => data === review.signature); |                     self._mangroveIdentity.kid.data, | ||||||
|                     const rev: Review = { |                     "reviews.kid is", | ||||||
|                         made_by_user: byUser, |                     review.kid | ||||||
|                         date: new Date(r.iat * 1000), |                 ) | ||||||
|                         comment: r.opinion, |                 const byUser = self._mangroveIdentity.kid.map((data) => data === review.signature) | ||||||
|                         author: r.metadata.nickname, |                 const rev: Review = { | ||||||
|                         affiliated: r.metadata.is_affiliated, |                     made_by_user: byUser, | ||||||
|                         rating: r.rating // percentage points
 |                     date: new Date(r.iat * 1000), | ||||||
|                     }; |                     comment: r.opinion, | ||||||
| 
 |                     author: r.metadata.nickname, | ||||||
| 
 |                     affiliated: r.metadata.is_affiliated, | ||||||
|                     (rev.made_by_user ? reviewsByUser : reviews).push(rev); |                     rating: r.rating, // percentage points
 | ||||||
|                 } |                 } | ||||||
|                 self._reviews.setData(reviewsByUser.concat(reviews)) | 
 | ||||||
|  |                 ;(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)) { |     AddReview(r: Review, callback?: () => void) { | ||||||
| 
 |         callback = | ||||||
| 
 |             callback ?? | ||||||
|         callback = callback ?? (() => { |             (() => { | ||||||
|             return undefined; |                 return undefined | ||||||
|         }); |             }) | ||||||
| 
 | 
 | ||||||
|         const payload = { |         const payload = { | ||||||
|             sub: this.GetSubjectUri(), |             sub: this.GetSubjectUri(), | ||||||
|  | @ -164,35 +169,29 @@ export default class MangroveReviews { | ||||||
|             opinion: r.comment, |             opinion: r.comment, | ||||||
|             metadata: { |             metadata: { | ||||||
|                 nickname: r.author, |                 nickname: r.author, | ||||||
|             } |             }, | ||||||
|         }; |         } | ||||||
|         if (r.affiliated) { |         if (r.affiliated) { | ||||||
|             // @ts-ignore
 |             // @ts-ignore
 | ||||||
|             payload.metadata.is_affiliated = true; |             payload.metadata.is_affiliated = true | ||||||
|         } |         } | ||||||
|         if (this._dryRun) { |         if (this._dryRun) { | ||||||
|             console.warn("DRYRUNNING mangrove reviews: ", payload); |             console.warn("DRYRUNNING mangrove reviews: ", payload) | ||||||
|             if (callback) { |             if (callback) { | ||||||
|                 if (callback) { |                 if (callback) { | ||||||
|                     callback(); |                     callback() | ||||||
|                 } |                 } | ||||||
|                 this._reviews.data.push(r); |                 this._reviews.data.push(r) | ||||||
|                 this._reviews.ping(); |                 this._reviews.ping() | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             mangrove.signAndSubmitReview(this._mangroveIdentity.keypair, payload).then(() => { |             mangrove.signAndSubmitReview(this._mangroveIdentity.keypair, payload).then(() => { | ||||||
|                 if (callback) { |                 if (callback) { | ||||||
|                     callback(); |                     callback() | ||||||
|                 } |                 } | ||||||
|                 this._reviews.data.push(r); |                 this._reviews.data.push(r) | ||||||
|                 this._reviews.ping(); |                 this._reviews.ping() | ||||||
| 
 |  | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | } | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -1,42 +1,52 @@ | ||||||
| /** | /** | ||||||
|  * Wraps the query parameters into UIEventSources |  * Wraps the query parameters into UIEventSources | ||||||
|  */ |  */ | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import Hash from "./Hash"; | import Hash from "./Hash" | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| export class QueryParameters { | export class QueryParameters { | ||||||
| 
 |  | ||||||
|     static defaults = {} |     static defaults = {} | ||||||
|     static documentation: Map<string, string> = new Map<string, string>() |     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 _wasInitialized: Set<string> = new Set() | ||||||
|     private static knownSources = {}; |     private static knownSources = {} | ||||||
|     private static initialized = false; |     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) { |         if (!this.initialized) { | ||||||
|             this.init(); |             this.init() | ||||||
|         } |         } | ||||||
|         QueryParameters.documentation.set(key, documentation); |         QueryParameters.documentation.set(key, documentation) | ||||||
|         if (deflt !== undefined) { |         if (deflt !== undefined) { | ||||||
|             QueryParameters.defaults[key] = deflt; |             QueryParameters.defaults[key] = deflt | ||||||
|         } |         } | ||||||
|         if (QueryParameters.knownSources[key] !== undefined) { |         if (QueryParameters.knownSources[key] !== undefined) { | ||||||
|             return QueryParameters.knownSources[key]; |             return QueryParameters.knownSources[key] | ||||||
|         } |         } | ||||||
|         QueryParameters.addOrder(key); |         QueryParameters.addOrder(key) | ||||||
|         const source = new UIEventSource<string>(deflt, "&" + key); |         const source = new UIEventSource<string>(deflt, "&" + key) | ||||||
|         QueryParameters.knownSources[key] = source; |         QueryParameters.knownSources[key] = source | ||||||
|         source.addCallback(() => QueryParameters.Serialize()) |         source.addCallback(() => QueryParameters.Serialize()) | ||||||
|         return source; |         return source | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static GetBooleanQueryParameter(key: string, deflt: boolean, documentation?: string): UIEventSource<boolean> { |     public static GetBooleanQueryParameter( | ||||||
|         return QueryParameters.GetQueryParameter(key, ""+ deflt, documentation).sync(str => str === "true", [], b => "" + b) |         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 { |     public static wasInitialized(key: string): boolean { | ||||||
|         return QueryParameters._wasInitialized.has(key) |         return QueryParameters._wasInitialized.has(key) | ||||||
|     } |     } | ||||||
|  | @ -48,53 +58,54 @@ export class QueryParameters { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static init() { |     private static init() { | ||||||
| 
 |  | ||||||
|         if (this.initialized) { |         if (this.initialized) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
|         this.initialized = true; |         this.initialized = true | ||||||
| 
 | 
 | ||||||
|         if (Utils.runningFromConsole) { |         if (Utils.runningFromConsole) { | ||||||
|             return; |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (window?.location?.search) { |         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) { |             for (const param of params) { | ||||||
|                 const kv = param.split("="); |                 const kv = param.split("=") | ||||||
|                 const key = decodeURIComponent(kv[0]); |                 const key = decodeURIComponent(kv[0]) | ||||||
|                 QueryParameters.addOrder(key) |                 QueryParameters.addOrder(key) | ||||||
|                 QueryParameters._wasInitialized.add(key) |                 QueryParameters._wasInitialized.add(key) | ||||||
|                 const v = decodeURIComponent(kv[1]); |                 const v = decodeURIComponent(kv[1]) | ||||||
|                 const source = new UIEventSource<string>(v); |                 const source = new UIEventSource<string>(v) | ||||||
|                 source.addCallback(() => QueryParameters.Serialize()) |                 source.addCallback(() => QueryParameters.Serialize()) | ||||||
|                 QueryParameters.knownSources[key] = source; |                 QueryParameters.knownSources[key] = source | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static Serialize() { |     private static Serialize() { | ||||||
|         const parts = [] |         const parts = [] | ||||||
|         for (const key of QueryParameters.order) { |         for (const key of QueryParameters.order) { | ||||||
|             if (QueryParameters.knownSources[key]?.data === undefined) { |             if (QueryParameters.knownSources[key]?.data === undefined) { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (QueryParameters.knownSources[key].data === "undefined") { |             if (QueryParameters.knownSources[key].data === "undefined") { | ||||||
|                 continue; |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (QueryParameters.knownSources[key].data === QueryParameters.defaults[key]) { |             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
 |             // 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,13 +1,13 @@ | ||||||
| import {Store} from "../UIEventSource"; | import { Store } from "../UIEventSource" | ||||||
| 
 | 
 | ||||||
| export interface Review { | export interface Review { | ||||||
|     comment?: string, |     comment?: string | ||||||
|     author: string, |     author: string | ||||||
|     date: Date, |     date: Date | ||||||
|     rating: number, |     rating: number | ||||||
|     affiliated: boolean, |     affiliated: boolean | ||||||
|     /** |     /** | ||||||
|      * True if the current logged in user is the creator of this comment |      * True if the current logged in user is the creator of this comment | ||||||
|      */ |      */ | ||||||
|     made_by_user: Store<boolean> |     made_by_user: Store<boolean> | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| import {UIEventSource} from "../UIEventSource"; | import { UIEventSource } from "../UIEventSource" | ||||||
| import * as wds from "wikidata-sdk" | import * as wds from "wikidata-sdk" | ||||||
| 
 | 
 | ||||||
| export class WikidataResponse { | export class WikidataResponse { | ||||||
|  | @ -18,14 +18,12 @@ export class WikidataResponse { | ||||||
|         wikisites: Map<string, string>, |         wikisites: Map<string, string>, | ||||||
|         commons: string |         commons: string | ||||||
|     ) { |     ) { | ||||||
| 
 |  | ||||||
|         this.id = id |         this.id = id | ||||||
|         this.labels = labels |         this.labels = labels | ||||||
|         this.descriptions = descriptions |         this.descriptions = descriptions | ||||||
|         this.claims = claims |         this.claims = claims | ||||||
|         this.wikisites = wikisites |         this.wikisites = wikisites | ||||||
|         this.commons = commons |         this.commons = commons | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static fromJson(entity: any): WikidataResponse { |     public static fromJson(entity: any): WikidataResponse { | ||||||
|  | @ -41,7 +39,7 @@ export class WikidataResponse { | ||||||
|             descr.set(labelName, entity.descriptions[labelName].value) |             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) { |         for (const labelName in entity.sitelinks) { | ||||||
|             // labelName is `${language}wiki`
 |             // labelName is `${language}wiki`
 | ||||||
|             const language = labelName.substring(0, labelName.length - 4) |             const language = labelName.substring(0, labelName.length - 4) | ||||||
|  | @ -51,28 +49,19 @@ export class WikidataResponse { | ||||||
| 
 | 
 | ||||||
|         const commons = sitelinks.get("commons") |         const commons = sitelinks.get("commons") | ||||||
|         sitelinks.delete("commons") |         sitelinks.delete("commons") | ||||||
|         const claims = WikidataResponse.extractClaims(entity.claims); |         const claims = WikidataResponse.extractClaims(entity.claims) | ||||||
|         return new WikidataResponse( |         return new WikidataResponse(entity.id, labels, descr, claims, sitelinks, commons) | ||||||
|             entity.id, |  | ||||||
|             labels, |  | ||||||
|             descr, |  | ||||||
|             claims, |  | ||||||
|             sitelinks, |  | ||||||
|             commons |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static extractClaims(claimsJson: any): Map<string, Set<string>> { |     static extractClaims(claimsJson: any): Map<string, Set<string>> { | ||||||
| 
 |  | ||||||
|         const simplified = wds.simplify.claims(claimsJson, { |         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) { |         for (const claimId in simplified) { | ||||||
|             const claimsList: any[] = simplified[claimId] |             const claimsList: any[] = simplified[claimId] | ||||||
|             claims.set(claimId, new Set(claimsList)); |             claims.set(claimId, new Set(claimsList)) | ||||||
|         } |         } | ||||||
|         return claims |         return claims | ||||||
|     } |     } | ||||||
|  | @ -84,7 +73,6 @@ export class WikidataLexeme { | ||||||
|     senses: Map<string, string> |     senses: Map<string, string> | ||||||
|     claims: Map<string, Set<string>> |     claims: Map<string, Set<string>> | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     constructor(json) { |     constructor(json) { | ||||||
|         this.id = json.id |         this.id = json.id | ||||||
|         this.claims = WikidataResponse.extractClaims(json.claims) |         this.claims = WikidataResponse.extractClaims(json.claims) | ||||||
|  | @ -117,36 +105,40 @@ export class WikidataLexeme { | ||||||
|             this.claims, |             this.claims, | ||||||
|             new Map(), |             new Map(), | ||||||
|             undefined |             undefined | ||||||
|         ); |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface WikidataSearchoptions { | export interface WikidataSearchoptions { | ||||||
|     lang?: "en" | string, |     lang?: "en" | string | ||||||
|     maxCount?: 20 | number |     maxCount?: 20 | number | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface WikidataAdvancedSearchoptions extends WikidataSearchoptions { | export interface WikidataAdvancedSearchoptions extends WikidataSearchoptions { | ||||||
|     instanceOf?: number[]; |     instanceOf?: number[] | ||||||
|     notInstanceOf?: number[] |     notInstanceOf?: number[] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  * Utility functions around wikidata |  * Utility functions around wikidata | ||||||
|  */ |  */ | ||||||
| export default class Wikidata { | export default class Wikidata { | ||||||
| 
 |     private static readonly _identifierPrefixes = ["Q", "L"].map((str) => str.toLowerCase()) | ||||||
|     private static readonly _identifierPrefixes = ["Q", "L"].map(str => str.toLowerCase()) |     private static readonly _prefixesToRemove = [ | ||||||
|     private static readonly _prefixesToRemove = ["https://www.wikidata.org/wiki/Lexeme:",  |         "https://www.wikidata.org/wiki/Lexeme:", | ||||||
|         "https://www.wikidata.org/wiki/", |         "https://www.wikidata.org/wiki/", | ||||||
|         "http://www.wikidata.org/entity/", |         "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 | ||||||
|     public static LoadWikidataEntry(value: string | number): UIEventSource<{ success: WikidataResponse } | { error: any }> { |     ): UIEventSource<{ success: WikidataResponse } | { error: any }> { | ||||||
|         const key = this.ExtractKey(value) |         const key = this.ExtractKey(value) | ||||||
|         const cached = Wikidata._cache.get(key) |         const cached = Wikidata._cache.get(key) | ||||||
|         if (cached !== undefined) { |         if (cached !== undefined) { | ||||||
|  | @ -154,27 +146,31 @@ export default class Wikidata { | ||||||
|         } |         } | ||||||
|         const src = UIEventSource.FromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key)) |         const src = UIEventSource.FromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key)) | ||||||
|         Wikidata._cache.set(key, src) |         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. |      * 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 |      * 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<{ |     public static async searchAdvanced( | ||||||
|         id: string, |         text: string, | ||||||
|         relevance?: number, |         options: WikidataAdvancedSearchoptions | ||||||
|         label: string, |     ): Promise< | ||||||
|         description?: string |         { | ||||||
|     }[]> { |             id: string | ||||||
|  |             relevance?: number | ||||||
|  |             label: string | ||||||
|  |             description?: string | ||||||
|  |         }[] | ||||||
|  |     > { | ||||||
|         let instanceOf = "" |         let instanceOf = "" | ||||||
|         if (options?.instanceOf !== undefined && options.instanceOf.length > 0) { |         if (options?.instanceOf !== undefined && options.instanceOf.length > 0) { | ||||||
|            const phrases = options.instanceOf.map(q => `{ ?item wdt:P31/wdt:P279* wd:Q${q}. }`) |             const phrases = options.instanceOf.map((q) => `{ ?item wdt:P31/wdt:P279* wd:Q${q}. }`) | ||||||
|             instanceOf = "{"+ phrases.join(" UNION ") + "}" |             instanceOf = "{" + phrases.join(" UNION ") + "}" | ||||||
|         } |         } | ||||||
|         const forbidden = (options?.notInstanceOf ?? []) |         const forbidden = (options?.notInstanceOf ?? []).concat([17379835]) // blacklist 'wikimedia pages outside of the main knowledge tree', e.g. disambiguation pages
 | ||||||
|             .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 minusPhrases = forbidden.map(q => `MINUS {?item wdt:P31/wdt:P279* wd:Q${q} .}`) |  | ||||||
|         const sparql = `SELECT * WHERE {
 |         const sparql = `SELECT * WHERE {
 | ||||||
|             SERVICE wikibase:mwapi { |             SERVICE wikibase:mwapi { | ||||||
|                 bd:serviceParam wikibase:api "EntitySearch" . |                 bd:serviceParam wikibase:api "EntitySearch" . | ||||||
|  | @ -183,7 +179,11 @@ export default class Wikidata { | ||||||
|                 bd:serviceParam mwapi:language "${options.lang}" . |                 bd:serviceParam mwapi:language "${options.lang}" . | ||||||
|                 ?item wikibase:apiOutputItem mwapi:item . |                 ?item wikibase:apiOutputItem mwapi:item . | ||||||
|                 ?num wikibase:apiOrdinal true . |                 ?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 . |                 ?label wikibase:apiOutput mwapi:label . | ||||||
|                 ?description wikibase:apiOutput "@description" . |                 ?description wikibase:apiOutput "@description" . | ||||||
|             }  |             }  | ||||||
|  | @ -195,11 +195,11 @@ export default class Wikidata { | ||||||
|         const result = await Utils.downloadJson(url) |         const result = await Utils.downloadJson(url) | ||||||
|         /*The full uri of the wikidata-item*/ |         /*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, |             relevance: num?.value, | ||||||
|             id: item?.value, |             id: item?.value, | ||||||
|             label: label?.value, |             label: label?.value, | ||||||
|             description: description?.value |             description: description?.value, | ||||||
|         })) |         })) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -207,47 +207,47 @@ export default class Wikidata { | ||||||
|         search: string, |         search: string, | ||||||
|         options?: WikidataSearchoptions, |         options?: WikidataSearchoptions, | ||||||
|         page = 1 |         page = 1 | ||||||
|     ): Promise<{ |     ): Promise< | ||||||
|         id: string, |         { | ||||||
|         label: string, |             id: string | ||||||
|         description: string |             label: string | ||||||
|     }[]> { |             description: string | ||||||
|  |         }[] | ||||||
|  |     > { | ||||||
|         const maxCount = options?.maxCount ?? 20 |         const maxCount = options?.maxCount ?? 20 | ||||||
|         let pageCount = Math.min(maxCount, 50) |         let pageCount = Math.min(maxCount, 50) | ||||||
|         const start = page * pageCount - pageCount; |         const start = page * pageCount - pageCount | ||||||
|         const lang = (options?.lang ?? "en") |         const lang = options?.lang ?? "en" | ||||||
|         const url = |         const url = | ||||||
|             "https://www.wikidata.org/w/api.php?action=wbsearchentities&search=" + |             "https://www.wikidata.org/w/api.php?action=wbsearchentities&search=" + | ||||||
|             search + |             search + | ||||||
|             "&language=" + |             "&language=" + | ||||||
|             lang + |             lang + | ||||||
|             "&limit=" + pageCount + "&continue=" + |             "&limit=" + | ||||||
|  |             pageCount + | ||||||
|  |             "&continue=" + | ||||||
|             start + |             start + | ||||||
|             "&format=json&uselang=" + |             "&format=json&uselang=" + | ||||||
|             lang + |             lang + | ||||||
|             "&type=item&origin=*" + |             "&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 response = await Utils.downloadJsonCached(url, 10000) | ||||||
| 
 | 
 | ||||||
|         const result: any[] = response.search |         const result: any[] = response.search | ||||||
| 
 | 
 | ||||||
|         if (result.length < pageCount) { |         if (result.length < pageCount) { | ||||||
|             // No next page
 |             // No next page
 | ||||||
|             return result; |             return result | ||||||
|         } |         } | ||||||
|         if (result.length < maxCount) { |         if (result.length < maxCount) { | ||||||
|             const newOptions = {...options} |             const newOptions = { ...options } | ||||||
|             newOptions.maxCount = maxCount - result.length |             newOptions.maxCount = maxCount - result.length | ||||||
|             result.push(...await Wikidata.search(search, |             result.push(...(await Wikidata.search(search, newOptions, page + 1))) | ||||||
|                 newOptions, |  | ||||||
|                 page + 1 |  | ||||||
|             )) |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return result; |         return result | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     public static async searchAndFetch( |     public static async searchAndFetch( | ||||||
|         search: string, |         search: string, | ||||||
|         options?: WikidataAdvancedSearchoptions |         options?: WikidataAdvancedSearchoptions | ||||||
|  | @ -255,16 +255,17 @@ export default class Wikidata { | ||||||
|         // We provide some padding to filter away invalid values
 |         // We provide some padding to filter away invalid values
 | ||||||
|         const searchResults = await Wikidata.searchAdvanced(search, options) |         const searchResults = await Wikidata.searchAdvanced(search, options) | ||||||
|         const maybeResponses = await Promise.all( |         const maybeResponses = await Promise.all( | ||||||
|             searchResults.map(async r => { |             searchResults.map(async (r) => { | ||||||
|                 try { |                 try { | ||||||
|                     console.log("Loading ", r.id) |                     console.log("Loading ", r.id) | ||||||
|                     return await Wikidata.LoadWikidataEntry(r.id).AsPromise() |                     return await Wikidata.LoadWikidataEntry(r.id).AsPromise() | ||||||
|                 } catch (e) { |                 } catch (e) { | ||||||
|                     console.error(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) { |         if (value === undefined) { | ||||||
|             console.error("ExtractKey: value is undefined") |             console.error("ExtractKey: value is undefined") | ||||||
|             return undefined; |             return undefined | ||||||
|         } |         } | ||||||
|         value = value.trim().toLowerCase() |         value = value.trim().toLowerCase() | ||||||
| 
 | 
 | ||||||
|  | @ -296,7 +297,7 @@ export default class Wikidata { | ||||||
| 
 | 
 | ||||||
|         for (const identifierPrefix of Wikidata._identifierPrefixes) { |         for (const identifierPrefix of Wikidata._identifierPrefixes) { | ||||||
|             if (value.startsWith(identifierPrefix)) { |             if (value.startsWith(identifierPrefix)) { | ||||||
|                 const trimmed = value.substring(identifierPrefix.length); |                 const trimmed = value.substring(identifierPrefix.length) | ||||||
|                 if (trimmed === "") { |                 if (trimmed === "") { | ||||||
|                     return undefined |                     return undefined | ||||||
|                 } |                 } | ||||||
|  | @ -304,7 +305,7 @@ export default class Wikidata { | ||||||
|                 if (isNaN(n)) { |                 if (isNaN(n)) { | ||||||
|                     return undefined |                     return undefined | ||||||
|                 } |                 } | ||||||
|                 return value.toUpperCase(); |                 return value.toUpperCase() | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -312,7 +313,7 @@ export default class Wikidata { | ||||||
|             return "Q" + value |             return "Q" + value | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return undefined; |         return undefined | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -326,10 +327,10 @@ export default class Wikidata { | ||||||
|      * Wikidata.QIdToNumber(123) // => 123
 |      * Wikidata.QIdToNumber(123) // => 123
 | ||||||
|      */ |      */ | ||||||
|     public static QIdToNumber(q: string | number): number | undefined { |     public static QIdToNumber(q: string | number): number | undefined { | ||||||
|         if(q === undefined || q === null){ |         if (q === undefined || q === null) { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         if(typeof q === "number"){ |         if (typeof q === "number") { | ||||||
|             return q |             return q | ||||||
|         } |         } | ||||||
|         q = q.trim() |         q = q.trim() | ||||||
|  | @ -356,17 +357,23 @@ export default class Wikidata { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Build a SPARQL-query, return the result |      * Build a SPARQL-query, return the result | ||||||
|      *  |      * | ||||||
|      * @param keys: how variables are named. Every key not ending with 'Label' should appear in at least one statement |      * @param keys: how variables are named. Every key not ending with 'Label' should appear in at least one statement | ||||||
|      * @param statements |      * @param statements | ||||||
|      * @constructor |      * @constructor | ||||||
|      */ |      */ | ||||||
|     public static async Sparql<T>(keys: string[], statements: string[]):Promise< (T & Record<string, {type: string, value: string}>) []> { |     public static async Sparql<T>( | ||||||
|         const query = "SELECT "+keys.map(k => k.startsWith("?") ? k : "?"+k).join(" ")+"\n" + |         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" + |             "WHERE\n" + | ||||||
|             "{\n" + |             "{\n" + | ||||||
|             statements.map(stmt => stmt.endsWith(".") ? stmt : stmt+".").join("\n") + |             statements.map((stmt) => (stmt.endsWith(".") ? stmt : stmt + ".")).join("\n") + | ||||||
|             "  SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE]\". }\n" + |             '  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE]". }\n' + | ||||||
|             "}" |             "}" | ||||||
|         const url = wds.sparqlQuery(query) |         const url = wds.sparqlQuery(query) | ||||||
|         const result = await Utils.downloadJsonCached(url, 24 * 60 * 60 * 1000) |         const result = await Utils.downloadJsonCached(url, 24 * 60 * 60 * 1000) | ||||||
|  | @ -384,7 +391,7 @@ export default class Wikidata { | ||||||
|             return undefined |             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 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 firstKey = <string>Array.from(Object.keys(entities))[0] // Roundabout way to fetch the entity; it might have been a redirect
 | ||||||
|         const response = entities[firstKey] |         const response = entities[firstKey] | ||||||
|  | @ -396,5 +403,4 @@ export default class Wikidata { | ||||||
| 
 | 
 | ||||||
|         return WikidataResponse.fromJson(response) |         return WikidataResponse.fromJson(response) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import {Utils} from "../../Utils"; | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| export default class Wikimedia { | export default class Wikimedia { | ||||||
|     /** |     /** | ||||||
|  | @ -8,40 +8,48 @@ export default class Wikimedia { | ||||||
|      * @param maxLoad: the maximum amount of images to return |      * @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 |      * @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( | ||||||
|                                             maxLoad = 10, |         categoryName: string, | ||||||
|                                             continueParameter: string = undefined): Promise<string[]> { |         maxLoad = 10, | ||||||
|  |         continueParameter: string = undefined | ||||||
|  |     ): Promise<string[]> { | ||||||
|         if (categoryName === undefined || categoryName === null || categoryName === "") { |         if (categoryName === undefined || categoryName === null || categoryName === "") { | ||||||
|             return []; |             return [] | ||||||
|         } |         } | ||||||
|         if (!categoryName.startsWith("Category:")) { |         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&" + |             "action=query&list=categorymembers&format=json&" + | ||||||
|             "&origin=*" + |             "&origin=*" + | ||||||
|             "&cmtitle=" + encodeURIComponent(categoryName); |             "&cmtitle=" + | ||||||
|  |             encodeURIComponent(categoryName) | ||||||
|         if (continueParameter !== undefined) { |         if (continueParameter !== undefined) { | ||||||
|             url = `${url}&cmcontinue=${continueParameter}`; |             url = `${url}&cmcontinue=${continueParameter}` | ||||||
|         } |         } | ||||||
|         const response = await Utils.downloadJson(url) |         const response = await Utils.downloadJson(url) | ||||||
|         const members = response.query?.categorymembers ?? []; |         const members = response.query?.categorymembers ?? [] | ||||||
|         const imageOverview: string[] = members.map(member => member.title); |         const imageOverview: string[] = members.map((member) => member.title) | ||||||
| 
 | 
 | ||||||
|         if (response.continue === undefined) { |         if (response.continue === undefined) { | ||||||
|             // We are done crawling through the category - no continuation in sight
 |             // We are done crawling through the category - no continuation in sight
 | ||||||
|             return imageOverview; |             return imageOverview | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (maxLoad - imageOverview.length <= 0) { |         if (maxLoad - imageOverview.length <= 0) { | ||||||
|             console.debug(`Recursive wikimedia category load stopped for ${categoryName}`) |             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
 |         // 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) |         imageOverview.push(...recursive) | ||||||
|         return imageOverview |         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