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…
Reference in a new issue