Merge branch 'develop'
This commit is contained in:
commit
f7002ce315
411 changed files with 54602 additions and 38696 deletions
10
.editorconfig
Normal file
10
.editorconfig
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
root = true
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
3
.git-blame-ignore-revs
Normal file
3
.git-blame-ignore-revs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# to be filled once final sha is known
|
||||||
|
# Prettier init
|
||||||
|
b541d3eab49761faf710893386e9bee2801ff533
|
|
@ -4,9 +4,11 @@ runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v1.4.6
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: "16"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
|
||||||
- name: install deps
|
- name: install deps
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
23
.github/workflows/deploy_pietervdvn.yml
vendored
23
.github/workflows/deploy_pietervdvn.yml
vendored
|
@ -1,6 +1,5 @@
|
||||||
name: Deployment on pietervdvn
|
name: Deployment on pietervdvn
|
||||||
on:
|
on: push
|
||||||
push
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -28,13 +27,15 @@ jobs:
|
||||||
cd pietervdvn.github.io
|
cd pietervdvn.github.io
|
||||||
git pull
|
git pull
|
||||||
|
|
||||||
|
- name: get branch name
|
||||||
|
run: echo TARGET_BRANCH=${GITHUB_REF:11} >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: "Copying files"
|
- name: "Copying files"
|
||||||
run: |
|
run: |
|
||||||
echo "Deploying"
|
echo "Deploying"
|
||||||
TARGET=${GITHUB_REF:11}
|
rm -rf pietervdvn.github.io/mc/${{ env.TARGET_BRANCH }}/*
|
||||||
rm -rf pietervdvn.github.io/mc/$TARGET/*
|
mkdir -p pietervdvn.github.io/mc/${{ env.TARGET_BRANCH }}/
|
||||||
mkdir -p pietervdvn.github.io/mc/$TARGET/
|
cp -r dist/* pietervdvn.github.io/mc/${{ env.TARGET_BRANCH }}/
|
||||||
cp -r dist/* pietervdvn.github.io/mc/$TARGET/
|
|
||||||
cd pietervdvn.github.io/
|
cd pietervdvn.github.io/
|
||||||
git add *
|
git add *
|
||||||
if git status | grep -q "Changes to be committed"
|
if git status | grep -q "Changes to be committed"
|
||||||
|
@ -44,3 +45,13 @@ jobs:
|
||||||
else
|
else
|
||||||
echo "No changes to commit"
|
echo "No changes to commit"
|
||||||
fi
|
fi
|
||||||
|
env:
|
||||||
|
TARGET_BRANCH: ${{ env.TARGET_BRANCH }}
|
||||||
|
|
||||||
|
- uses: mshick/add-pr-comment@v1
|
||||||
|
name: Comment the PR with the review URL
|
||||||
|
if: ${{ success() && github.ref != 'refs/heads/develop' && github.ref != 'refs/heads/master' }}
|
||||||
|
with:
|
||||||
|
message: |
|
||||||
|
[🚀 Preview Branch](https://pietervdvn.github.io/mc/${{ env.TARGET_BRANCH }})
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
2
.github/workflows/validate-pr.yml
vendored
2
.github/workflows/validate-pr.yml
vendored
|
@ -6,7 +6,7 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup and validate themes
|
- name: Setup and validate themes
|
||||||
uses: ./.github/actions/setup-and-validate
|
uses: ./.github/actions/setup-and-validate
|
||||||
|
|
13
.prettierignore
Normal file
13
.prettierignore
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
langs/
|
||||||
|
vendor/
|
||||||
|
dist/
|
||||||
|
.cache
|
||||||
|
assets/generated/
|
||||||
|
assets/themes/
|
||||||
|
assets/layers/
|
||||||
|
Docs/Tools/stats/
|
||||||
|
Docs/Layers/
|
||||||
|
Docs/Schemas/
|
||||||
|
Docs/TagInfo/
|
4
.prettierrc.json
Normal file
4
.prettierrc.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
|
@ -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, 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) {
|
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,26 +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 = []
|
||||||
for (let day = 1; day <= 31; day++) {
|
let monthIsFinished = true
|
||||||
|
const writtenFiles = []
|
||||||
|
for (let day = startDay; day <= 31; day++) {
|
||||||
if (year === currentYear && month === currentMonth && day === today.getDate()) {
|
if (year === currentYear && month === currentMonth && day === today.getDate()) {
|
||||||
break;
|
monthIsFinished = false
|
||||||
|
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)
|
||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
features.push(...JSON.parse(readFileSync(path, "UTF-8")))
|
let features = JSON.parse(readFileSync(path, "UTF-8"))
|
||||||
console.log("Loaded ", path, "from disk, got", features.length, "features now")
|
features = features?.features ?? features
|
||||||
|
console.log(features)
|
||||||
|
features.push(...features.features) // day-stats are generally a list already, but in some ad-hoc cases might be a geojson-collection too
|
||||||
|
console.log(
|
||||||
|
"Loaded ",
|
||||||
|
path,
|
||||||
|
"from disk, got",
|
||||||
|
features.length,
|
||||||
|
"features now"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let dayFeatures: any[] = undefined
|
let dayFeatures: any[] = undefined
|
||||||
|
@ -59,47 +71,72 @@ 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)
|
||||||
|
|
||||||
}
|
}
|
||||||
writeFileSync(pathM, JSON.stringify({features}))
|
if (monthIsFinished) {
|
||||||
|
writeFileSync(pathM, JSON.stringify({ features }))
|
||||||
|
for (const writtenFile of writtenFiles) {
|
||||||
|
unlinkSync(writtenFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startDay = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
public async DownloadStatsForDay(
|
||||||
|
year: number,
|
||||||
public async DownloadStatsForDay(year: number, month: number, day: number, path: string): Promise<any[]> {
|
month: number,
|
||||||
|
day: number,
|
||||||
let page = 1;
|
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)
|
||||||
|
@ -107,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")
|
||||||
|
@ -167,38 +205,48 @@ async function main(): Promise<void> {
|
||||||
const targetDir = "Docs/Tools/stats"
|
const targetDir = "Docs/Tools/stats"
|
||||||
let year = 2020
|
let year = 2020
|
||||||
let month = 5
|
let month = 5
|
||||||
if(!isNaN(Number(process.argv[2]))){
|
let day = 1
|
||||||
|
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]))) {
|
||||||
|
day = Number(process.argv[4])
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try {
|
try {
|
||||||
|
await new StatsDownloader(targetDir).DownloadStats(year, month, day)
|
||||||
await new StatsDownloader(targetDir).DownloadStats(year, month)
|
|
||||||
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,32 +1 @@
|
||||||
[
|
["file-overview.json","missing_editor.json","stats.2020-10.json","stats.2020-11.json","stats.2020-12.json","stats.2020-5.json","stats.2020-6.json","stats.2020-7.json","stats.2020-8.json","stats.2020-9.json","stats.2021-1.json","stats.2021-10.json","stats.2021-11.json","stats.2021-12.json","stats.2021-2.json","stats.2021-3.json","stats.2021-4.json","stats.2021-5.json","stats.2021-6.json","stats.2021-7.json","stats.2021-8.json","stats.2021-9.json","stats.2022-1.json","stats.2022-2.json","stats.2022-3.json","stats.2022-4.json","stats.2022-5.json","stats.2022-6.json","stats.2022-7.json","stats.2022-8.json","stats.2022-9-01.day.json","stats.2022-9-02.day.json"]
|
||||||
"file-overview.json",
|
|
||||||
"missing_editor.json",
|
|
||||||
"stats.2020-10.json",
|
|
||||||
"stats.2020-11.json",
|
|
||||||
"stats.2020-12.json",
|
|
||||||
"stats.2020-5.json",
|
|
||||||
"stats.2020-6.json",
|
|
||||||
"stats.2020-7.json",
|
|
||||||
"stats.2020-8.json",
|
|
||||||
"stats.2020-9.json",
|
|
||||||
"stats.2021-1.json",
|
|
||||||
"stats.2021-10.json",
|
|
||||||
"stats.2021-11.json",
|
|
||||||
"stats.2021-12.json",
|
|
||||||
"stats.2021-2.json",
|
|
||||||
"stats.2021-3.json",
|
|
||||||
"stats.2021-4.json",
|
|
||||||
"stats.2021-5.json",
|
|
||||||
"stats.2021-6.json",
|
|
||||||
"stats.2021-7.json",
|
|
||||||
"stats.2021-8.json",
|
|
||||||
"stats.2021-9.json",
|
|
||||||
"stats.2022-1.json",
|
|
||||||
"stats.2022-2.json",
|
|
||||||
"stats.2022-3.json",
|
|
||||||
"stats.2022-4.json",
|
|
||||||
"stats.2022-5.json",
|
|
||||||
"stats.2022-6.json",
|
|
||||||
"stats.2022-7.json",
|
|
||||||
"stats.2022-8.json"
|
|
||||||
]
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
Docs/Tools/stats/stats.2022-9-01.day.json
Normal file
1
Docs/Tools/stats/stats.2022-9-01.day.json
Normal file
File diff suppressed because one or more lines are too long
1
Docs/Tools/stats/stats.2022-9-02.day.json
Normal file
1
Docs/Tools/stats/stats.2022-9-02.day.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -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",
|
id: "osm",
|
||||||
name: "OpenStreetMap",
|
name: "OpenStreetMap",
|
||||||
layer: () => AvailableBaseLayersImplementation.CreateBackgroundLayer("osm", "OpenStreetMap",
|
layer: () =>
|
||||||
"https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright",
|
AvailableBaseLayersImplementation.CreateBackgroundLayer(
|
||||||
|
"osm",
|
||||||
|
"OpenStreetMap",
|
||||||
|
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
|
"OpenStreetMap",
|
||||||
|
"https://openStreetMap.org/copyright",
|
||||||
19,
|
19,
|
||||||
false, false),
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
feature: null,
|
feature: null,
|
||||||
max_zoom: 19,
|
max_zoom: 19,
|
||||||
min_zoom: 0,
|
min_zoom: 0,
|
||||||
isBest: true, // Of course, OpenStreetMap is the best map!
|
isBest: true, // Of course, OpenStreetMap is the best map!
|
||||||
category: "osmbasedmap"
|
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,8 +79,8 @@ 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,
|
||||||
|
@ -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: () =>
|
||||||
|
L.tileLayer.provider(id, {
|
||||||
maxNativeZoom: layer.options?.maxZoom,
|
maxNativeZoom: layer.options?.maxZoom,
|
||||||
maxZoom: Math.max(layer.options?.maxZoom ?? 19, 21)
|
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) {
|
if (!a.isBest) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (a.category !== category) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
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,35 +1,33 @@
|
||||||
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<Coordinates>;
|
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>(
|
||||||
const currentGPSLocation = new UIEventSource<Coordinates>(undefined, "GPS-coordinate")
|
undefined,
|
||||||
|
"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,38 +141,37 @@ 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(() => {
|
||||||
/*
|
/*
|
||||||
|
@ -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) {
|
|
||||||
console.log("Geolocation permission is ", status.state);
|
|
||||||
if (status.state === "granted") {
|
if (status.state === "granted") {
|
||||||
self.StartGeolocating(zoomToLocation);
|
self.StartGeolocating(zoomToLocation)
|
||||||
}
|
}
|
||||||
self._permission.setData(status.state);
|
self._permission.setData(status.state)
|
||||||
status.onchange = function () {
|
status.onchange = function () {
|
||||||
self._permission.setData(status.state);
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
...Tiles.MapRange(neededTiles, (x, y) => {
|
||||||
|
const date = freshness.freshnessFor(padToZoomLevel, x, y)
|
||||||
if (date === undefined) {
|
if (date === undefined) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return date.getTime()
|
return date.getTime()
|
||||||
})) / 1000;
|
})
|
||||||
|
) / 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
|
|
||||||
.stabilized(10000)
|
|
||||||
.addCallback(feature => {
|
|
||||||
if (feature === undefined) {
|
if (feature === undefined) {
|
||||||
// The popup got closed - we flush
|
// The popup got closed - we flush
|
||||||
changes.flushChanges("Flushing changes due to popup closed");
|
changes.flushChanges("Flushing changes due to popup closed")
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
if (Utils.runningFromConsole) {
|
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(
|
||||||
|
"visibilitychange",
|
||||||
|
() => {
|
||||||
changes.flushChanges("Visibility change")
|
changes.flushChanges("Visibility change")
|
||||||
}, false);
|
},
|
||||||
|
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
|
||||||
|
osmConnection: OsmConnection
|
||||||
layoutToUse: LayoutConfig
|
layoutToUse: LayoutConfig
|
||||||
}, latestTags: any, id: string
|
},
|
||||||
|
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,23 +21,28 @@ 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
|
||||||
}
|
}
|
||||||
|
|
148
Logic/BBox.ts
148
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,7 +67,10 @@ 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],
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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,100 +44,111 @@ 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,
|
||||||
|
@ -146,18 +159,21 @@ export default class DetermineLayout {
|
||||||
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)
|
||||||
|
|
||||||
|
@ -165,27 +181,27 @@ export default class DetermineLayout {
|
||||||
|
|
||||||
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(
|
||||||
|
params: ExtraFuncParams,
|
||||||
feature: any,
|
feature: any,
|
||||||
features: string | any[],
|
features: string | any[],
|
||||||
options?: { maxFeatures?: number, uniqueTag?: string | undefined, maxDistance?: number }): { feat: any, distance: number }[] {
|
options?: { maxFeatures?: number; uniqueTag?: string | undefined; maxDistance?: number }
|
||||||
|
): { feat: any; distance: number }[] {
|
||||||
const maxFeatures = options?.maxFeatures ?? 1
|
const 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
|
||||||
|
) {
|
||||||
|
this._featurePipeline = featurePipeline
|
||||||
|
this._state = state
|
||||||
|
|
||||||
if(state.currentView !== undefined){
|
if (state.currentView !== undefined) {
|
||||||
const currentViewUpdater = new MetatagUpdater(state.currentView, this._state, this._featurePipeline)
|
const currentViewUpdater = new MetatagUpdater(
|
||||||
|
state.currentView,
|
||||||
|
this._state,
|
||||||
|
this._featurePipeline
|
||||||
|
)
|
||||||
this._alreadyRegistered.add(state.currentView)
|
this._alreadyRegistered.add(state.currentView)
|
||||||
this._notifiers.push(currentViewUpdater)
|
this._notifiers.push(currentViewUpdater)
|
||||||
state.currentView.features.addCallback(_ => {
|
state.currentView.features.addCallback((_) => {
|
||||||
console.debug("Requesting an update for currentView")
|
console.debug("Requesting an update for currentView")
|
||||||
currentViewUpdater.updateMetaTags();
|
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>,
|
||||||
|
location: UIEventSource<Loc>,
|
||||||
registerFreshness: (tileId: number, freshness: Date) => void,
|
registerFreshness: (tileId: number, freshness: Date) => void,
|
||||||
registerTile: ((src: FeatureSource & Tiled) => void)) {
|
registerTile: (src: FeatureSource & Tiled) => void
|
||||||
const self = this;
|
) {
|
||||||
|
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,7 +131,14 @@ 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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
let minzoom = Math.min(
|
||||||
return location.zoom >= minzoom;
|
...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(
|
|
||||||
filteredLayer,
|
|
||||||
tile => {
|
|
||||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||||
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))
|
||||||
});
|
})
|
||||||
continue;
|
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,21 +273,24 @@ 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,
|
||||||
|
(source) =>
|
||||||
|
TiledFeatureSource.createHierarchy(source, {
|
||||||
layer: source.layer,
|
layer: source.layer,
|
||||||
minZoomLevel: source.layer.layerDef.minzoom,
|
minZoomLevel: source.layer.layerDef.minzoom,
|
||||||
noDuplicates: true,
|
noDuplicates: true,
|
||||||
|
@ -287,87 +299,102 @@ export default class FeaturePipeline {
|
||||||
registerTile: (tile) => {
|
registerTile: (tile) => {
|
||||||
// We save the tile data for the given layer to local storage - data sourced from overpass
|
// We save the tile data for the given layer to local storage - data sourced from overpass
|
||||||
self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile)
|
self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile)
|
||||||
perLayerHierarchy.get(source.layer.layerDef.id).registerTile(new RememberingSource(tile))
|
perLayerHierarchy
|
||||||
tile.features.addCallbackAndRunD(f => {
|
.get(source.layer.layerDef.id)
|
||||||
|
.registerTile(new RememberingSource(tile))
|
||||||
|
tile.features.addCallbackAndRunD((f) => {
|
||||||
if (f.length === 0) {
|
if (f.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.onNewDataLoaded(tile)
|
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(state.changes, state.allElements, state.osmConnection._oauth_config.url)
|
const newGeometry = new NewGeometryFromChangesFeatureSource(
|
||||||
this.newGeometryHandler = newGeometry;
|
state.changes,
|
||||||
newGeometry.features.addCallbackAndRun(geometries => {
|
state.allElements,
|
||||||
|
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) {
|
||||||
|
@ -432,12 +467,13 @@ export default class FeaturePipeline {
|
||||||
* */
|
* */
|
||||||
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(
|
||||||
|
(bbox) => {
|
||||||
if (bbox === undefined) {
|
if (bbox === undefined) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
if (!isSufficientlyZoomed.data) {
|
if (!isSufficientlyZoomed.data) {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
const osmSourceZoomLevel = self.osmSourceZoomLevel
|
const osmSourceZoomLevel = self.osmSourceZoomLevel
|
||||||
const range = bbox.containingTileRange(osmSourceZoomLevel)
|
const range = bbox.containingTileRange(osmSourceZoomLevel)
|
||||||
|
@ -447,30 +483,42 @@ export default class FeaturePipeline {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
Tiles.MapRange(range, (x, y) => {
|
Tiles.MapRange(range, (x, y) => {
|
||||||
const i = Tiles.tile_index(osmSourceZoomLevel, x, y);
|
const i = Tiles.tile_index(osmSourceZoomLevel, x, y)
|
||||||
const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y)
|
const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y)
|
||||||
if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) {
|
if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) {
|
||||||
console.debug("Skipping tile", osmSourceZoomLevel, x, y, "as a decently fresh one is available")
|
console.debug(
|
||||||
|
"Skipping tile",
|
||||||
|
osmSourceZoomLevel,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
"as a decently fresh one is available"
|
||||||
|
)
|
||||||
// The cached tiles contain decently fresh data
|
// The cached tiles contain decently fresh data
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
tileIndexes.push(i)
|
tileIndexes.push(i)
|
||||||
})
|
})
|
||||||
return tileIndexes
|
return tileIndexes
|
||||||
}, [isSufficientlyZoomed])
|
},
|
||||||
|
[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>
|
||||||
|
): OverpassFeatureSource {
|
||||||
|
const minzoom = Math.min(...state.layoutToUse.layers.map((layer) => layer.minzoom))
|
||||||
|
const overpassIsActive = state.currentBounds.map(
|
||||||
|
(bbox) => {
|
||||||
if (bbox === undefined) {
|
if (bbox === undefined) {
|
||||||
console.debug("Disabling overpass source: no bbox")
|
console.debug("Disabling overpass source: no bbox")
|
||||||
return false
|
return false
|
||||||
|
@ -479,7 +527,7 @@ export default class FeaturePipeline {
|
||||||
if (zoom < minzoom) {
|
if (zoom < minzoom) {
|
||||||
// We are zoomed out over the zoomlevel of any layer
|
// We are zoomed out over the zoomlevel of any layer
|
||||||
console.debug("Disabling overpass source: zoom < minzoom")
|
console.debug("Disabling overpass source: zoom < minzoom")
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const range = bbox.containingTileRange(zoom)
|
const range = bbox.containingTileRange(zoom)
|
||||||
|
@ -487,58 +535,64 @@ export default class FeaturePipeline {
|
||||||
// Let's assume we don't have so much data cached
|
// Let's assume we don't have so much data cached
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const self = this;
|
const self = this
|
||||||
const allFreshnesses = Tiles.MapRange(range, (x, y) => self.freshnessForVisibleLayers(zoom, x, y))
|
const allFreshnesses = Tiles.MapRange(range, (x, y) =>
|
||||||
return allFreshnesses.some(freshness => freshness === undefined || freshness < this.oldestAllowedDate)
|
self.freshnessForVisibleLayers(zoom, x, y)
|
||||||
}, [state.locationControl])
|
)
|
||||||
|
return allFreshnesses.some(
|
||||||
|
(freshness) => freshness === undefined || freshness < this.oldestAllowedDate
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[state.locationControl]
|
||||||
|
)
|
||||||
|
|
||||||
const self = this;
|
const self = this
|
||||||
const updater = new OverpassFeatureSource(state,
|
const updater = new OverpassFeatureSource(state, {
|
||||||
{
|
padToTiles: state.locationControl.map((l) => Math.min(15, l.zoom + 1)),
|
||||||
padToTiles: state.locationControl.map(l => Math.min(15, l.zoom + 1)),
|
|
||||||
relationTracker: this.relationTracker,
|
relationTracker: this.relationTracker,
|
||||||
isActive: useOsmApi.map(b => !b && overpassIsActive.data, [overpassIsActive]),
|
isActive: useOsmApi.map((b) => !b && overpassIsActive.data, [overpassIsActive]),
|
||||||
freshnesses: this.freshnesses,
|
freshnesses: this.freshnesses,
|
||||||
onBboxLoaded: (bbox, date, downloadedLayers, paddedToZoomLevel) => {
|
onBboxLoaded: (bbox, date, downloadedLayers, paddedToZoomLevel) => {
|
||||||
Tiles.MapRange(bbox.containingTileRange(paddedToZoomLevel), (x, y) => {
|
Tiles.MapRange(bbox.containingTileRange(paddedToZoomLevel), (x, y) => {
|
||||||
const tileIndex = Tiles.tile_index(paddedToZoomLevel, x, y)
|
const tileIndex = Tiles.tile_index(paddedToZoomLevel, x, y)
|
||||||
downloadedLayers.forEach(layer => {
|
downloadedLayers.forEach((layer) => {
|
||||||
self.freshnesses.get(layer.id).addTileLoad(tileIndex, date)
|
self.freshnesses.get(layer.id).addTileLoad(tileIndex, date)
|
||||||
self.localStorageSavers.get(layer.id)?.MarkVisited(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,126 +1,122 @@
|
||||||
/**
|
/**
|
||||||
* 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,
|
public constructor(
|
||||||
|
flayer: FilteredLayer,
|
||||||
zxy?: [number, number, number] | BBox,
|
zxy?: [number, number, number] | BBox,
|
||||||
options?: {
|
options?: {
|
||||||
featureIdBlacklist?: Set<string>
|
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]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,39 +126,38 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,9 +1,10 @@
|
||||||
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"
|
||||||
|
|
||||||
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'
|
||||||
|
@ -14,36 +15,36 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
||||||
* These elements are probably created by the 'SimpleAddUi' which generates a new point, but the import functionality might create a line or polygon too.
|
* 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)
|
||||||
|
|
||||||
|
@ -59,38 +60,36 @@ 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 = {}
|
const tags: OsmTags = {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
tags["id"] = change.type + "/" + change.id
|
|
||||||
|
|
||||||
tags["_backend"] = backendUrl
|
tags["_backend"] = backendUrl
|
||||||
|
|
||||||
|
@ -102,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,8 +65,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
@ -69,62 +75,59 @@ export default class RenderingMultiPlexerFeatureSource {
|
||||||
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(
|
this.features = upstream.features.map((features) => {
|
||||||
features => {
|
|
||||||
if (features === undefined) {
|
if (features === undefined) {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const withIndex: any[] = []
|
||||||
const withIndex: any[] = [];
|
|
||||||
|
|
||||||
function addAsPoint(feat, rendering, coordinate) {
|
function addAsPoint(feat, rendering, coordinate) {
|
||||||
const patched = {
|
const patched = {
|
||||||
...feat,
|
...feat,
|
||||||
pointRenderingIndex: rendering.index
|
pointRenderingIndex: rendering.index,
|
||||||
}
|
}
|
||||||
patched.geometry = {
|
patched.geometry = {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
coordinates: coordinate
|
coordinates: coordinate,
|
||||||
}
|
}
|
||||||
withIndex.push(patched)
|
withIndex.push(patched)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for (const f of features) {
|
for (const f of features) {
|
||||||
const feat = f.feature;
|
const feat = f.feature
|
||||||
if(feat === undefined){
|
if (feat === undefined) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
this.inspectFeature(feat, addAsPoint, withIndex)
|
this.inspectFeature(feat, addAsPoint, withIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 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"
|
||||||
|
) {
|
||||||
|
const now = new Date()
|
||||||
|
return new StaticFeatureSource(
|
||||||
|
featureSource.map((features) =>
|
||||||
|
features.map((feature) => ({
|
||||||
feature: feature.feature,
|
feature: feature.feature,
|
||||||
freshness: now
|
freshness: now,
|
||||||
}))), name);
|
}))
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
layer: FilteredLayer,
|
||||||
registerLayer: (layer: FeatureSourceForLayer & Tiled) => void,
|
registerLayer: (layer: FeatureSourceForLayer & Tiled) => void,
|
||||||
state: {
|
state: {
|
||||||
locationControl?: UIEventSource<{zoom?: number}>
|
locationControl?: UIEventSource<{ zoom?: number }>
|
||||||
currentBounds: UIEventSource<BBox>
|
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,31 +1,32 @@
|
||||||
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(
|
||||||
|
(bounds) => {
|
||||||
if (bounds === undefined) {
|
if (bounds === undefined) {
|
||||||
// We'll retry later
|
// We'll retry later
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -33,32 +34,47 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor
|
||||||
|
|
||||||
if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) {
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.locationControl?.data?.zoom !== undefined && state.locationControl.data.zoom < layer.layerDef.minzoom) {
|
|
||||||
// No need to download! - the layer is disabled
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tileRange = Tiles.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
|
|
||||||
if (tileRange.total > 10000) {
|
|
||||||
console.error("Got a really big tilerange, bounds and location might be out of sync")
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i))
|
if (
|
||||||
|
state.locationControl?.data?.zoom !== undefined &&
|
||||||
|
state.locationControl.data.zoom < layer.layerDef.minzoom
|
||||||
|
) {
|
||||||
|
// No need to download! - the layer is disabled
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const tileRange = Tiles.TileRangeBetween(
|
||||||
|
zoomlevel,
|
||||||
|
bounds.getNorth(),
|
||||||
|
bounds.getEast(),
|
||||||
|
bounds.getSouth(),
|
||||||
|
bounds.getWest()
|
||||||
|
)
|
||||||
|
if (tileRange.total > 10000) {
|
||||||
|
console.error(
|
||||||
|
"Got a really big tilerange, bounds and location might be out of sync"
|
||||||
|
)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const needed = Tiles.MapRange(tileRange, (x, y) =>
|
||||||
|
Tiles.tile_index(zoomlevel, x, y)
|
||||||
|
).filter((i) => !self._loadedTiles.has(i))
|
||||||
if (needed.length === 0) {
|
if (needed.length === 0) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return needed
|
return needed
|
||||||
}
|
},
|
||||||
, [layer.isDisplayed, state.locationControl]).stabilized(250);
|
[layer.isDisplayed, state.locationControl]
|
||||||
|
)
|
||||||
|
.stabilized(250)
|
||||||
|
|
||||||
neededTiles.addCallbackAndRunD(neededIndexes => {
|
neededTiles.addCallbackAndRunD((neededIndexes) => {
|
||||||
console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes)
|
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){
|
|
||||||
console.error("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(
|
||||||
|
"PANIC: got the tile from the OSM-api, but something crashed handling this tile"
|
||||||
|
)
|
||||||
|
error = e
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
"Could not download tile",
|
||||||
|
z,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
"due to",
|
||||||
|
e,
|
||||||
|
"; retrying with smaller bounds"
|
||||||
|
)
|
||||||
if (e === "rate limited") {
|
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 = []
|
||||||
|
@ -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,76 +96,95 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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 []
|
||||||
|
@ -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"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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 [
|
||||||
|
Promise.resolve({
|
||||||
key: key,
|
key: key,
|
||||||
url: value,
|
url: value,
|
||||||
provider: this
|
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(
|
||||||
|
allTags: Store<any>,
|
||||||
|
options?: {
|
||||||
prefixes?: string[]
|
prefixes?: string[]
|
||||||
}): UIEventSource<ProvidedImage[]> {
|
}
|
||||||
|
): 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,100 +1,105 @@
|
||||||
import $ from "jquery"
|
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 settings = {
|
|
||||||
async: true,
|
|
||||||
crossDomain: true,
|
|
||||||
processData: false,
|
|
||||||
contentType: false,
|
|
||||||
type: 'POST',
|
|
||||||
url: apiUrl,
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Client-ID ' + apiKey,
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
mimeType: 'multipart/form-data',
|
|
||||||
};
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('image', blob);
|
|
||||||
formData.append("title", title);
|
|
||||||
formData.append("description", description)
|
formData.append("description", description)
|
||||||
// @ts-ignore
|
|
||||||
settings.data = formData;
|
const settings: RequestInit = {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
redirect: "follow",
|
||||||
|
headers: new Headers({
|
||||||
|
Authorization: `Client-ID ${apiKey}`,
|
||||||
|
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) {
|
||||||
|
const content = await response.json()
|
||||||
|
await handleSuccessfullUpload(content.data.link)
|
||||||
|
})
|
||||||
|
.catch((reason) => {
|
||||||
|
console.log("Uploading to IMGUR failed", reason)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
$.ajax(settings).done(async function (response) {
|
onFail(reason)
|
||||||
response = JSON.parse(response);
|
})
|
||||||
await handleSuccessfullUpload(response.data.link);
|
|
||||||
}).fail((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 [
|
||||||
|
Promise.resolve({
|
||||||
url: value,
|
url: value,
|
||||||
key: key,
|
key: key,
|
||||||
provider: this
|
provider: this,
|
||||||
})]
|
}),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -110,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,15 +1,20 @@
|
||||||
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"]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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,23 +1,23 @@
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,11 +29,11 @@ export default class Maproulette {
|
||||||
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(
|
||||||
|
features: { feature: any; freshness: Date }[],
|
||||||
params: ExtraFuncParams,
|
params: ExtraFuncParams,
|
||||||
layer: LayerConfig,
|
layer: LayerConfig,
|
||||||
state?: { allElements?: ElementStorage },
|
state?: { allElements?: ElementStorage },
|
||||||
options?: {
|
options?: {
|
||||||
includeDates?: true | boolean,
|
includeDates?: true | boolean
|
||||||
includeNonDates?: true | boolean,
|
includeNonDates?: true | boolean
|
||||||
evaluateStrict?: false | boolean
|
evaluateStrict?: false | boolean
|
||||||
}): 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,35 +51,40 @@ 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
|
||||||
|
@ -91,12 +95,18 @@ export default class MetaTagging {
|
||||||
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,10 +1,9 @@
|
||||||
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
|
||||||
*/
|
*/
|
||||||
|
@ -12,7 +11,7 @@ export interface ChangeDescription {
|
||||||
/*
|
/*
|
||||||
* 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,16 +50,19 @@ 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,
|
| {
|
||||||
|
lat: number
|
||||||
lon: number
|
lon: number
|
||||||
} | {
|
}
|
||||||
|
| {
|
||||||
/* Coordinates are only used for rendering. They should be LON, LAT
|
/* Coordinates are only used for rendering. They should be LON, LAT
|
||||||
* */
|
* */
|
||||||
coordinates: [number, number][]
|
coordinates: [number, number][]
|
||||||
nodes: number[],
|
nodes: number[]
|
||||||
} | {
|
}
|
||||||
members: { type: "node" | "way" | "relation", ref: number, role: string }[]
|
| {
|
||||||
|
members: { type: "node" | "way" | "relation"; ref: number; role: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -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
|
||||||
*
|
*
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,42 @@
|
||||||
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,
|
||||||
|
newLonLat: [number, number],
|
||||||
|
meta: {
|
||||||
|
theme: string
|
||||||
reason: string
|
reason: string
|
||||||
}) {
|
}
|
||||||
super(id, true);
|
) {
|
||||||
|
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(
|
||||||
|
elementId: string,
|
||||||
tagsFilter: TagsFilter,
|
tagsFilter: TagsFilter,
|
||||||
currentTags: Record<string, string>, meta: {
|
currentTags: Record<string, string>,
|
||||||
theme: string,
|
meta: {
|
||||||
|
theme: string
|
||||||
changeType: "answer" | "soft-delete" | "add-image" | string
|
changeType: "answer" | "soft-delete" | "add-image" | string
|
||||||
}) {
|
}
|
||||||
super(elementId, true);
|
) {
|
||||||
this._elementId = elementId;
|
super(elementId, true)
|
||||||
this._tagsFilter = tagsFilter;
|
this._elementId = elementId
|
||||||
this._currentTags = currentTags;
|
this._tagsFilter = tagsFilter
|
||||||
this._meta = meta;
|
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,
|
type: <"node" | "way" | "relation">type,
|
||||||
id: id,
|
id: id,
|
||||||
tags: changedTags,
|
tags: changedTags,
|
||||||
meta: this._meta
|
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(
|
||||||
|
tags: Tag[],
|
||||||
outerRingCoordinates: [number, number][],
|
outerRingCoordinates: [number, number][],
|
||||||
innerRingsCoordinates: [number, number][][],
|
innerRingsCoordinates: [number, number][][],
|
||||||
state: FeaturePipelineState,
|
state: FeaturePipelineState,
|
||||||
config: MergePointConfig[],
|
config: MergePointConfig[],
|
||||||
changeType: "import" | "create" | string
|
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,
|
||||||
|
lon: number,
|
||||||
options: {
|
options: {
|
||||||
allowReuseOfPreviouslyCreatedPoints?: boolean,
|
allowReuseOfPreviouslyCreatedPoints?: boolean
|
||||||
snapOnto?: OsmWay,
|
snapOnto?: OsmWay
|
||||||
reusePointWithinMeters?: number,
|
reusePointWithinMeters?: number
|
||||||
theme: string,
|
theme: string
|
||||||
changeType: "create" | "import" | null,
|
changeType: "create" | "import" | null
|
||||||
specialMotivation?: string
|
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,15 +125,19 @@ 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),
|
tags: new And(this._basicTags).asChange(properties),
|
||||||
type: "node",
|
type: "node",
|
||||||
id: reusedPointId,
|
id: reusedPointId,
|
||||||
meta: this.meta
|
meta: this.meta,
|
||||||
}]
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const locations = [...this._snapOnto.coordinates.map(([lat, lon]) =><[number,number]> [lon, lat])]
|
const locations = [
|
||||||
|
...this._snapOnto.coordinates.map(([lat, lon]) => <[number, number]>[lon, lat]),
|
||||||
|
]
|
||||||
const ids = [...this._snapOnto.nodes]
|
const ids = [...this._snapOnto.nodes]
|
||||||
|
|
||||||
locations.splice(index + 1, 0, [this._lon, this._lat])
|
locations.splice(index + 1, 0, [this._lon, this._lat])
|
||||||
|
@ -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(
|
||||||
|
tags: Tag[],
|
||||||
|
coordinates: { nodeId?: number; lat: number; lon: number }[],
|
||||||
options: {
|
options: {
|
||||||
theme: string
|
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(
|
||||||
|
tags: Tag[],
|
||||||
coordinates: [number, number][],
|
coordinates: [number, number][],
|
||||||
state: FeaturePipelineState,
|
state: FeaturePipelineState,
|
||||||
config: MergePointConfig[]
|
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,
|
meta: this.meta,
|
||||||
doDelete: true,
|
doDelete: true,
|
||||||
type: osmObject.type,
|
type: osmObject.type,
|
||||||
id: osmObject.id,
|
id: osmObject.id,
|
||||||
}]
|
},
|
||||||
|
]
|
||||||
} else {
|
} else {
|
||||||
return await new ChangeTagAction(
|
return await new ChangeTagAction(this._id, this._softDeletionTags, osmObject.tags, {
|
||||||
this._id, this._softDeletionTags, osmObject.tags,
|
|
||||||
{
|
|
||||||
...this.meta,
|
...this.meta,
|
||||||
changeType: "soft-delete"
|
changeType: "soft-delete",
|
||||||
}
|
}).CreateChangeDescriptions(changes)
|
||||||
).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
|
||||||
|
.map((nodes, i) => ({
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
id: this._input.allWayIdsInOrder[i]
|
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,
|
id: relation.id,
|
||||||
type: "relation",
|
type: "relation",
|
||||||
changes: {members: newMembers},
|
changes: { members: newMembers },
|
||||||
meta: {
|
meta: {
|
||||||
changeType: "relation-fix",
|
changeType: "relation-fix",
|
||||||
theme: this._theme
|
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,
|
||||||
|
{
|
||||||
|
reason: string
|
||||||
hasTags: boolean
|
hasTags: boolean
|
||||||
}>,
|
}
|
||||||
reprojectedNodes: Map<number, {
|
>
|
||||||
|
reprojectedNodes: Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
|
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
|
||||||
projectAfterIndex: number,
|
projectAfterIndex: number
|
||||||
distance: number,
|
distance: number
|
||||||
newLat: number,
|
newLat: number
|
||||||
newLon: number,
|
newLon: number
|
||||||
nodeId: 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*/,
|
||||||
|
{
|
||||||
|
distances: number[]
|
||||||
// Part of some other way then the one that should be replaced
|
// Part of some other way then the one that should be replaced
|
||||||
partOfWay: boolean,
|
partOfWay: boolean
|
||||||
hasTags: 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,
|
||||||
|
{
|
||||||
|
reason: string
|
||||||
hasTags: boolean
|
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<
|
||||||
|
number,
|
||||||
|
{
|
||||||
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
|
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
|
||||||
projectAfterIndex: number,
|
projectAfterIndex: number
|
||||||
distance: number,
|
distance: number
|
||||||
newLat: number,
|
newLat: number
|
||||||
newLon: number,
|
newLon: number
|
||||||
nodeId: 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(way, [node.lon, node.lat])
|
||||||
const projected = GeoOperations.nearestPoint(
|
|
||||||
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(
|
|
||||||
[],
|
|
||||||
lat, lon,
|
|
||||||
{
|
|
||||||
allowReuseOfPreviouslyCreatedPoints: true,
|
allowReuseOfPreviouslyCreatedPoints: true,
|
||||||
theme: this.theme, changeType: null
|
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,12 +52,12 @@ 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)
|
||||||
|
@ -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,
|
relation: relation,
|
||||||
allWayIdsInOrder: allWayIdsInOrder,
|
allWayIdsInOrder: allWayIdsInOrder,
|
||||||
originalNodes: originalNodes,
|
originalNodes: originalNodes,
|
||||||
allWaysNodesInOrder: allWaysNodesInOrder,
|
allWaysNodesInOrder: allWaysNodesInOrder,
|
||||||
originalWayId: originalElement.id,
|
originalWayId: originalElement.id,
|
||||||
}, this._meta.theme).CreateChangeDescriptions(changes)
|
},
|
||||||
|
this._meta.theme
|
||||||
|
).CreateChangeDescriptions(changes)
|
||||||
changeDescription.push(...changDescrs)
|
changeDescription.push(...changDescrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,15 +187,15 @@ 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,
|
||||||
|
@ -196,32 +203,31 @@ export default class SplitAction extends OsmChangeAction {
|
||||||
// `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(
|
||||||
|
csId: string,
|
||||||
allChanges: {
|
allChanges: {
|
||||||
modifiedObjects: OsmObject[],
|
modifiedObjects: OsmObject[]
|
||||||
newObjects: OsmObject[],
|
newObjects: OsmObject[]
|
||||||
deletedObjects: OsmObject[]
|
deletedObjects: OsmObject[]
|
||||||
}): string {
|
}
|
||||||
|
): 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) =>
|
||||||
|
Math.min(
|
||||||
|
...recentLocationPoints.map((gpsPoint) => {
|
||||||
const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint)
|
const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint)
|
||||||
return GeoOperations.distanceBetween(coor, otherCoor)
|
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(
|
||||||
|
(descr) =>
|
||||||
|
descr.meta.changeType !== undefined && descr.meta.changeType !== null
|
||||||
|
)
|
||||||
|
.map((descr) => descr.meta.changeType)
|
||||||
|
),
|
||||||
|
([key, count]) => ({
|
||||||
key: key,
|
key: key,
|
||||||
value: count,
|
value: count,
|
||||||
aggregate: true
|
aggregate: true,
|
||||||
}))
|
})
|
||||||
const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined)
|
)
|
||||||
.map(descr => ({
|
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,7 +294,8 @@ export class Changes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const perBinMessage = Utils.NoNull(perBinCount.map((count, i) => {
|
const perBinMessage = Utils.NoNull(
|
||||||
|
perBinCount.map((count, i) => {
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
@ -268,9 +307,10 @@ export class Changes {
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
value: count,
|
value: count,
|
||||||
aggregate: true
|
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,28 +319,29 @@ export class Changes {
|
||||||
comment += "\n\n" + this.extraComment.data
|
comment += "\n\n" + this.extraComment.data
|
||||||
}
|
}
|
||||||
|
|
||||||
const metatags: ChangesetTag[] = [{
|
const metatags: ChangesetTag[] = [
|
||||||
|
{
|
||||||
key: "comment",
|
key: "comment",
|
||||||
value: 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)
|
||||||
|
@ -311,14 +352,14 @@ export class Changes {
|
||||||
)
|
)
|
||||||
|
|
||||||
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(
|
||||||
|
(str) => {
|
||||||
|
const n = Number(str)
|
||||||
if (isNaN(n)) {
|
if (isNaN(n)) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return n
|
return n
|
||||||
}, [], n => "" + n
|
},
|
||||||
);
|
[],
|
||||||
console.log("Using current-open-changeset-" + theme + " from the preferences, got " + openChangeset.data)
|
(n) => "" + n
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
"Using current-open-changeset-" +
|
||||||
|
theme +
|
||||||
|
" from the preferences, got " +
|
||||||
|
openChangeset.data
|
||||||
|
)
|
||||||
|
|
||||||
return await self.flushSelectChanges(pendingChanges, openChangeset);
|
return await self.flushSelectChanges(pendingChanges, openChangeset)
|
||||||
} catch (e) {
|
} 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 (
|
||||||
|
!Utils.Identical(nmembers, r.members, (a, b) => {
|
||||||
return a.role === b.role && a.type === b.type && a.ref === b.ref
|
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(
|
||||||
|
dryRun: UIEventSource<boolean>,
|
||||||
osmConnection: OsmConnection,
|
osmConnection: OsmConnection,
|
||||||
allElements: ElementStorage,
|
allElements: ElementStorage,
|
||||||
changes: Changes,
|
changes: Changes,
|
||||||
auth) {
|
auth
|
||||||
this.osmConnection = osmConnection;
|
) {
|
||||||
this.allElements = allElements;
|
this.osmConnection = osmConnection
|
||||||
this.changes = changes;
|
this.allElements = allElements
|
||||||
this._dryRun = dryRun;
|
this.changes = changes
|
||||||
this.userDetails = osmConnection.userDetails;
|
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,9 +112,12 @@ 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`"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,30 +125,35 @@ export class ChangesetHandler {
|
||||||
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(
|
||||||
|
extraMetaTags: ChangesetTag[],
|
||||||
rewriteIds: Map<string, string>,
|
rewriteIds: Map<string, string>,
|
||||||
oldChangesetMeta: {
|
oldChangesetMeta: {
|
||||||
open: boolean,
|
open: boolean
|
||||||
id: number
|
id: number
|
||||||
uid: number, // User ID
|
uid: number // User ID
|
||||||
changes_count: number,
|
changes_count: number
|
||||||
tags: any
|
tags: any
|
||||||
}) : ChangesetTag[] {
|
}
|
||||||
|
): 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,18 +261,18 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -284,8 +290,8 @@ 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")
|
||||||
|
@ -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",
|
||||||
|
},
|
||||||
|
function (err, response) {
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
|
console.log("err", err)
|
||||||
console.log("err", err);
|
|
||||||
}
|
}
|
||||||
console.log("Closed changeset ", changesetId)
|
console.log("Closed changeset ", changesetId)
|
||||||
resolve()
|
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,
|
|
||||||
`</changeset></osm>`].join("")
|
|
||||||
}, function (err, response) {
|
|
||||||
if (response === undefined) {
|
if (response === undefined) {
|
||||||
console.error("Updating the tags of changeset "+csId+" failed:", err);
|
console.error("Updating the tags of changeset " + csId + " failed:", err)
|
||||||
reject(err)
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
resolve(response);
|
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,
|
||||||
|
},
|
||||||
|
function (err, response) {
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
console.error("Uploading an actual change failed", err);
|
console.error("Uploading an actual change failed", err)
|
||||||
reject(err);
|
reject(err)
|
||||||
}
|
}
|
||||||
const changes = self.parseUploadChangesetResponse(response);
|
const changes = self.parseUploadChangesetResponse(response)
|
||||||
console.log("Uploaded changeset ", changesetId);
|
console.log("Uploaded changeset ", changesetId)
|
||||||
resolve(changes);
|
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=" +
|
||||||
|
query
|
||||||
return Utils.downloadJson(url)
|
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 userDetails: UIEventSource<UserDetails>
|
||||||
}
|
|
||||||
public auth;
|
|
||||||
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._dryRun = options.dryRun ?? new UIEventSource<boolean>(false)
|
||||||
|
|
||||||
this.updateAuthObject();
|
this.updateAuthObject()
|
||||||
|
|
||||||
this.preferencesHandler = new OsmPreferences(this.auth, this);
|
this.preferencesHandler = new OsmPreferences(this.auth, this)
|
||||||
|
|
||||||
if (options.oauth_token?.data !== undefined) {
|
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,17 +163,19 @@ 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",
|
||||||
|
},
|
||||||
|
function (err, details) {
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
console.log(err);
|
console.log(err)
|
||||||
self.loadingStatus.setData("error")
|
self.loadingStatus.setData("error")
|
||||||
if (err.status == 401) {
|
if (err.status == 401) {
|
||||||
console.log("Clearing tokens...")
|
console.log("Clearing tokens...")
|
||||||
|
@ -174,57 +184,60 @@ export class OsmConnection {
|
||||||
const tokens = [
|
const tokens = [
|
||||||
"https://www.openstreetmap.orgoauth_request_token_secret",
|
"https://www.openstreetmap.orgoauth_request_token_secret",
|
||||||
"https://www.openstreetmap.orgoauth_token",
|
"https://www.openstreetmap.orgoauth_token",
|
||||||
"https://www.openstreetmap.orgoauth_token_secret"]
|
"https://www.openstreetmap.orgoauth_token_secret",
|
||||||
tokens.forEach(token => localStorage.removeItem(token))
|
]
|
||||||
|
tokens.forEach((token) => localStorage.removeItem(token))
|
||||||
}
|
}
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (details == null) {
|
if (details == null) {
|
||||||
self.loadingStatus.setData("error")
|
self.loadingStatus.setData("error")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.CheckForMessagesContinuously();
|
self.CheckForMessagesContinuously()
|
||||||
|
|
||||||
// details is an XML DOM of user details
|
// details is an XML DOM of user details
|
||||||
let userInfo = details.getElementsByTagName("user")[0];
|
let userInfo = details.getElementsByTagName("user")[0]
|
||||||
|
|
||||||
// let moreDetails = new DOMParser().parseFromString(userInfo.innerHTML, "text/xml");
|
// let moreDetails = new DOMParser().parseFromString(userInfo.innerHTML, "text/xml");
|
||||||
|
|
||||||
let data = self.userDetails.data;
|
let data = self.userDetails.data
|
||||||
data.loggedIn = true;
|
data.loggedIn = true
|
||||||
console.log("Login completed, userinfo is ", userInfo);
|
console.log("Login completed, userinfo is ", userInfo)
|
||||||
data.name = userInfo.getAttribute('display_name');
|
data.name = userInfo.getAttribute("display_name")
|
||||||
data.uid = Number(userInfo.getAttribute("id"))
|
data.uid = Number(userInfo.getAttribute("id"))
|
||||||
data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count");
|
data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count")
|
||||||
|
|
||||||
data.img = undefined;
|
data.img = undefined
|
||||||
const imgEl = userInfo.getElementsByTagName("img");
|
const imgEl = userInfo.getElementsByTagName("img")
|
||||||
if (imgEl !== undefined && imgEl[0] !== undefined) {
|
if (imgEl !== undefined && imgEl[0] !== undefined) {
|
||||||
data.img = imgEl[0].getAttribute("href");
|
data.img = imgEl[0].getAttribute("href")
|
||||||
}
|
}
|
||||||
data.img = data.img ?? Img.AsData(Svg.osm_logo);
|
data.img = data.img ?? Img.AsData(Svg.osm_logo)
|
||||||
|
|
||||||
const homeEl = userInfo.getElementsByTagName("home");
|
const homeEl = userInfo.getElementsByTagName("home")
|
||||||
if (homeEl !== undefined && homeEl[0] !== undefined) {
|
if (homeEl !== undefined && homeEl[0] !== undefined) {
|
||||||
const lat = parseFloat(homeEl[0].getAttribute("lat"));
|
const lat = parseFloat(homeEl[0].getAttribute("lat"))
|
||||||
const lon = parseFloat(homeEl[0].getAttribute("lon"));
|
const lon = parseFloat(homeEl[0].getAttribute("lon"))
|
||||||
data.home = {lat: lat, lon: lon};
|
data.home = { lat: lat, lon: lon }
|
||||||
}
|
}
|
||||||
|
|
||||||
self.loadingStatus.setData("logged-in")
|
self.loadingStatus.setData("logged-in")
|
||||||
const messages = userInfo.getElementsByTagName("messages")[0].getElementsByTagName("received")[0];
|
const messages = userInfo
|
||||||
data.unreadMessages = parseInt(messages.getAttribute("unread"));
|
.getElementsByTagName("messages")[0]
|
||||||
data.totalMessages = parseInt(messages.getAttribute("count"));
|
.getElementsByTagName("received")[0]
|
||||||
|
data.unreadMessages = parseInt(messages.getAttribute("unread"))
|
||||||
|
data.totalMessages = parseInt(messages.getAttribute("count"))
|
||||||
|
|
||||||
self.userDetails.ping();
|
self.userDetails.ping()
|
||||||
for (const action of self._onLoggedIn) {
|
for (const action of self._onLoggedIn) {
|
||||||
action(self.userDetails.data);
|
action(self.userDetails.data)
|
||||||
}
|
}
|
||||||
self._onLoggedIn = [];
|
self._onLoggedIn = []
|
||||||
|
}
|
||||||
});
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeNote(id: number | string, text?: string): Promise<void> {
|
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',
|
{
|
||||||
|
method: "POST",
|
||||||
path: `/api/0.6/notes/${id}/close${textSuffix}`,
|
path: `/api/0.6/notes/${id}/close${textSuffix}`,
|
||||||
}, function (err, _) {
|
},
|
||||||
|
function (err, _) {
|
||||||
if (err !== null) {
|
if (err !== null) {
|
||||||
error(err)
|
error(err)
|
||||||
} else {
|
} else {
|
||||||
ok()
|
ok()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public reopenNote(id: number | string, text?: string): Promise<void> {
|
public reopenNote(id: number | string, text?: string): Promise<void> {
|
||||||
|
@ -259,50 +273,52 @@ export class OsmConnection {
|
||||||
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text)
|
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}`,
|
||||||
|
},
|
||||||
|
function (err, _) {
|
||||||
if (err !== null) {
|
if (err !== null) {
|
||||||
error(err)
|
error(err)
|
||||||
} else {
|
} else {
|
||||||
ok()
|
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 auth = this.auth
|
||||||
const content = {lat, lon, text}
|
const content = { lat, lon, text }
|
||||||
return new Promise((ok, error) => {
|
return new Promise((ok, error) => {
|
||||||
auth.xhr({
|
auth.xhr(
|
||||||
method: 'POST',
|
{
|
||||||
|
method: "POST",
|
||||||
path: `/api/0.6/notes.json`,
|
path: `/api/0.6/notes.json`,
|
||||||
options: {
|
options: {
|
||||||
header:
|
header: { "Content-Type": "application/json" },
|
||||||
{'Content-Type': 'application/json'}
|
|
||||||
},
|
},
|
||||||
content: JSON.stringify(content)
|
content: JSON.stringify(content),
|
||||||
|
},
|
||||||
}, function (
|
function (err, response: string) {
|
||||||
err,
|
|
||||||
response: string) {
|
|
||||||
console.log("RESPONSE IS", response)
|
console.log("RESPONSE IS", response)
|
||||||
if (err !== null) {
|
if (err !== null) {
|
||||||
error(err)
|
error(err)
|
||||||
|
@ -310,12 +326,11 @@ export class OsmConnection {
|
||||||
const parsed = JSON.parse(response)
|
const parsed = JSON.parse(response)
|
||||||
const id = parsed.properties.id
|
const id = parsed.properties.id
|
||||||
console.log("OPENED NOTE", id)
|
console.log("OPENED NOTE", id)
|
||||||
ok({id})
|
ok({ id })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public addCommentToNote(id: number | string, text: string): Promise<void> {
|
public addCommentToNote(id: number | string, text: string): Promise<void> {
|
||||||
|
@ -323,46 +338,53 @@ export class OsmConnection {
|
||||||
console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id)
|
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, _) {
|
},
|
||||||
|
function (err, _) {
|
||||||
if (err !== null) {
|
if (err !== null) {
|
||||||
error(err)
|
error(err)
|
||||||
} else {
|
} else {
|
||||||
ok()
|
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,32 +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"
|
||||||
|
|
||||||
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: {} = {};
|
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}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,58 +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: WayId): Promise<OsmWay | undefined>
|
||||||
|
static async DownloadObjectAsync(id: RelationId): Promise<OsmRelation | undefined>
|
||||||
|
static async DownloadObjectAsync(id: OsmId): Promise<OsmObject | undefined>
|
||||||
|
static async DownloadObjectAsync(id: string): Promise<OsmObject | undefined>
|
||||||
static async DownloadObjectAsync(id: string): Promise<OsmObject | undefined> {
|
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
|
||||||
|
@ -97,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
|
||||||
|
).then((data) => {
|
||||||
|
return data.elements.map((wayInfo) => {
|
||||||
const way = new OsmWay(wayInfo.id)
|
const way = new OsmWay(wayInfo.id)
|
||||||
way.LoadData(wayInfo)
|
way.LoadData(wayInfo)
|
||||||
return way
|
return way
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -123,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)
|
||||||
|
@ -132,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) {
|
||||||
|
@ -213,7 +226,7 @@ export abstract class OsmObject {
|
||||||
osmObject?.LoadData(element)
|
osmObject?.LoadData(element)
|
||||||
objects.push(osmObject)
|
objects.push(osmObject)
|
||||||
}
|
}
|
||||||
return objects;
|
return objects
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -227,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
|
||||||
}
|
}
|
||||||
|
@ -243,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"] = 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() {
|
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
|
||||||
|
|
||||||
|
@ -410,87 +446,95 @@ 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) {
|
function updateData(l: number) {
|
||||||
if(Object.keys(self.preferences.data).length === 0){
|
if (Object.keys(self.preferences.data).length === 0) {
|
||||||
// The preferences are still empty - they are not yet updated, so we delay updating for now
|
// The preferences are still empty - they are not yet updated, so we delay updating for now
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const prefsCount = Number(l);
|
const prefsCount = Number(l)
|
||||||
if (prefsCount > 100) {
|
if (prefsCount > 100) {
|
||||||
throw "Length to long";
|
throw "Length to long"
|
||||||
}
|
}
|
||||||
let str = "";
|
let str = ""
|
||||||
for (let i = 0; i < prefsCount; i++) {
|
for (let i = 0; i < prefsCount; i++) {
|
||||||
const key = allStartWith + "-" + i
|
const key = allStartWith + "-" + i
|
||||||
if(self.preferences.data[key] === undefined){
|
if (self.preferences.data[key] === undefined) {
|
||||||
console.warn("Detected a broken combined preference:", key, "is undefined", self.preferences)
|
console.warn(
|
||||||
|
"Detected a broken combined preference:",
|
||||||
|
key,
|
||||||
|
"is undefined",
|
||||||
|
self.preferences
|
||||||
|
)
|
||||||
}
|
}
|
||||||
str += self.preferences.data[key] ?? "";
|
str += self.preferences.data[key] ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
source.setData(str);
|
source.setData(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
length.addCallback(l => {
|
length.addCallback((l) => {
|
||||||
updateData(Number(l));
|
updateData(Number(l))
|
||||||
});
|
})
|
||||||
this.preferences.addCallbackAndRun(_ => {
|
this.preferences.addCallbackAndRun((_) => {
|
||||||
updateData(Number(length.data));
|
updateData(Number(length.data))
|
||||||
})
|
})
|
||||||
|
|
||||||
return source;
|
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);
|
const pref = new UIEventSource<string>(
|
||||||
|
this.preferences.data[key] ?? defaultValue,
|
||||||
|
"osm-preference:" + key
|
||||||
|
)
|
||||||
pref.addCallback((v) => {
|
pref.addCallback((v) => {
|
||||||
this.UploadPreference(key, 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,41 +162,42 @@ 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",
|
||||||
|
},
|
||||||
|
function (error, value: XMLDocument) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log("Could not load preferences", error);
|
console.log("Could not load preferences", error)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const prefs = value.getElementsByTagName("preference");
|
const prefs = value.getElementsByTagName("preference")
|
||||||
for (let i = 0; i < prefs.length; i++) {
|
for (let i = 0; i < prefs.length; i++) {
|
||||||
const pref = prefs[i];
|
const pref = prefs[i]
|
||||||
const k = pref.getAttribute("k");
|
const k = pref.getAttribute("k")
|
||||||
const v = pref.getAttribute("v");
|
const v = pref.getAttribute("v")
|
||||||
self.preferences.data[k] = v;
|
self.preferences.data[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// We merge all the preferences: new keys are uploaded
|
// We merge all the preferences: new keys are uploaded
|
||||||
// For differing values, the server overrides local changes
|
// For differing values, the server overrides local changes
|
||||||
self.preferenceSources.forEach((preference, key) => {
|
self.preferenceSources.forEach((preference, key) => {
|
||||||
const osmValue = self.preferences.data[key]
|
const osmValue = self.preferences.data[key]
|
||||||
if(osmValue === undefined && preference.data !== undefined){
|
if (osmValue === undefined && preference.data !== undefined) {
|
||||||
// OSM doesn't know this value yet
|
// OSM doesn't know this value yet
|
||||||
self.UploadPreference(key, preference.data)
|
self.UploadPreference(key, preference.data)
|
||||||
} else {
|
} else {
|
||||||
|
@ -192,51 +206,54 @@ export class OsmPreferences {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
self.preferences.ping();
|
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" } },
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.warn("Could not remove preference", error);
|
console.warn("Could not remove preference", error)
|
||||||
return;
|
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) {
|
},
|
||||||
|
function (error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.warn(`Could not set preference "${k}"'`, error);
|
console.warn(`Could not set preference "${k}"'`, error)
|
||||||
return;
|
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(
|
||||||
|
filter: TagsFilter,
|
||||||
extraScripts: string[],
|
extraScripts: string[],
|
||||||
interpreterUrl: string,
|
interpreterUrl: string,
|
||||||
timeout?: Store<number>,
|
timeout?: Store<number>,
|
||||||
relationTracker?: RelationsTracker,
|
relationTracker?: RelationsTracker,
|
||||||
includeMeta = true) {
|
includeMeta = true
|
||||||
this._timeout = timeout ?? new ImmutableStore<number>(90);
|
) {
|
||||||
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,9 +69,9 @@ 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]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,50 +86,54 @@ export class Overpass {
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
@ -126,9 +141,9 @@ export class Overpass {
|
||||||
/**
|
/**
|
||||||
* 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)}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) => {
|
||||||
|
if (typeof part === "object") {
|
||||||
const calculatedValue = this.interpret(part, tags)
|
const calculatedValue = this.interpret(part, tags)
|
||||||
return parseFloat(calculatedValue)
|
return parseFloat(calculatedValue)
|
||||||
} else {
|
} else {
|
||||||
return parseFloat(part);
|
return parseFloat(part)
|
||||||
}
|
}
|
||||||
}).filter(v => !isNaN(v));
|
})
|
||||||
return Math.min(...minArr);
|
.filter((v) => !isNaN(v))
|
||||||
|
return Math.min(...minArr)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getMaxValue(tags, subprogram) {
|
private static getMaxValue(tags, subprogram) {
|
||||||
const maxArr = subprogram.map(part => {
|
const maxArr = subprogram
|
||||||
if (typeof (part) === 'object') {
|
.map((part) => {
|
||||||
|
if (typeof part === "object") {
|
||||||
return parseFloat(AspectedRouting.interpret(part, tags))
|
return parseFloat(AspectedRouting.interpret(part, tags))
|
||||||
} else {
|
} else {
|
||||||
return parseFloat(part);
|
return parseFloat(part)
|
||||||
}
|
}
|
||||||
}).filter(v => !isNaN(v));
|
})
|
||||||
return Math.max(...maxArr);
|
.filter((v) => !isNaN(v))
|
||||||
|
return Math.max(...maxArr)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static concatMap(list, f): any[] {
|
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(
|
||||||
|
tags["opening_hours"],
|
||||||
|
{
|
||||||
lat: lat,
|
lat: lat,
|
||||||
lon: lon,
|
lon: lon,
|
||||||
address: {
|
address: {
|
||||||
country_code: tags._country.toLowerCase(),
|
country_code: tags._country.toLowerCase(),
|
||||||
state: undefined
|
state: undefined,
|
||||||
}
|
},
|
||||||
}, <any>{tag_key: "opening_hours"});
|
},
|
||||||
|
<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,76 +1,80 @@
|
||||||
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,
|
||||||
|
deflt: number,
|
||||||
|
docs: string
|
||||||
|
): UIEventSource<number> {
|
||||||
const localStorage = LocalStorageSource.Get(key)
|
const localStorage = LocalStorageSource.Get(key)
|
||||||
const previousValue = localStorage.data
|
const previousValue = localStorage.data
|
||||||
const src = UIEventSource.asFloat(
|
const src = UIEventSource.asFloat(
|
||||||
QueryParameters.GetQueryParameter(
|
QueryParameters.GetQueryParameter(key, "" + deflt, docs).syncWith(localStorage)
|
||||||
key,
|
)
|
||||||
"" + deflt,
|
|
||||||
docs
|
|
||||||
).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;
|
return src
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Location control initialization
|
// -- Location control initialization
|
||||||
const zoom = localStorageSynced("z",(layoutToUse?.startZoom ?? 1),"The initial/current zoom level")
|
const zoom = localStorageSynced(
|
||||||
const lat = localStorageSynced("lat",(layoutToUse?.startLat ?? 0),"The initial/current latitude")
|
"z",
|
||||||
const lon = localStorageSynced("lon",(layoutToUse?.startLon ?? 0),"The initial/current longitude of the app")
|
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),
|
||||||
|
@ -79,11 +83,9 @@ export default class ElementsState extends FeatureSwitchState {
|
||||||
})
|
})
|
||||||
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:
|
||||||
|
@ -52,25 +54,25 @@ export default class FeaturePipelineState extends MapState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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,54 +94,54 @@ 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)
|
||||||
|
@ -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(
|
||||||
|
QueryParameters.GetQueryParameter(
|
||||||
|
"overpassTimeout",
|
||||||
"" + layoutToUse?.overpassTimeout,
|
"" + layoutToUse?.overpassTimeout,
|
||||||
"Set a different timeout (in seconds) for queries in overpass"))
|
"Set a different timeout (in seconds) for queries in overpass"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
this.overpassMaxZoom = UIEventSource.asFloat(
|
||||||
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,
|
||||||
|
c.defaultState,
|
||||||
|
"Wether or not the overlay " + c.id + " is shown"
|
||||||
|
),
|
||||||
})) ?? []
|
})) ?? []
|
||||||
this.filteredLayers = new UIEventSource<FilteredLayer[]>( MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection))
|
this.filteredLayers = new UIEventSource<FilteredLayer[]>(
|
||||||
|
MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection)
|
||||||
|
)
|
||||||
|
|
||||||
this.lockBounds()
|
this.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,17 +193,19 @@ 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(
|
||||||
|
(bounds) => {
|
||||||
if (bounds === undefined) {
|
if (bounds === undefined) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -208,48 +216,59 @@ export default class MapState extends UserRelatedState {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties: {
|
properties: {
|
||||||
id: "current_view-" + i,
|
id: "current_view-" + i,
|
||||||
"current_view": "yes",
|
current_view: "yes",
|
||||||
"zoom": "" + self.locationControl.data.zoom
|
zoom: "" + self.locationControl.data.zoom,
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "Polygon",
|
type: "Polygon",
|
||||||
coordinates: [[
|
coordinates: [
|
||||||
|
[
|
||||||
[bounds.maxLon, bounds.maxLat],
|
[bounds.maxLon, bounds.maxLat],
|
||||||
[bounds.minLon, bounds.maxLat],
|
[bounds.minLon, bounds.maxLat],
|
||||||
[bounds.minLon, bounds.minLat],
|
[bounds.minLon, bounds.minLat],
|
||||||
[bounds.maxLon, bounds.minLat],
|
[bounds.maxLon, bounds.minLat],
|
||||||
[bounds.maxLon, bounds.maxLat],
|
[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,
|
feature,
|
||||||
freshness: new Date()
|
freshness: new Date(),
|
||||||
}]
|
},
|
||||||
|
]
|
||||||
})
|
})
|
||||||
let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_track")[0]
|
let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter(
|
||||||
|
(l) => l.layerDef.id === "gps_track"
|
||||||
|
)[0]
|
||||||
if (gpsLineLayerDef !== undefined) {
|
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: {
|
feature: {
|
||||||
"type": "Feature",
|
type: "Feature",
|
||||||
"properties": {
|
properties: {
|
||||||
"id": "home",
|
id: "home",
|
||||||
"user:home": "yes",
|
"user:home": "yes",
|
||||||
"_lon": homeLonLat[0],
|
_lon: homeLonLat[0],
|
||||||
"_lat": homeLonLat[1]
|
_lat: homeLonLat[1],
|
||||||
},
|
},
|
||||||
"geometry": {
|
geometry: {
|
||||||
"type": "Point",
|
type: "Point",
|
||||||
"coordinates": homeLonLat
|
coordinates: homeLonLat,
|
||||||
}
|
},
|
||||||
}, freshness: new Date()
|
},
|
||||||
}]
|
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,51 +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 {post} from "jquery";
|
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,
|
||||||
|
@ -55,78 +52,82 @@ 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(
|
||||||
|
@ -135,34 +136,36 @@ export default class UserRelatedState extends ElementsState {
|
||||||
"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
|
id: string
|
||||||
icon: string,
|
icon: string
|
||||||
title: any,
|
title: any
|
||||||
shortDescription: any,
|
shortDescription: any
|
||||||
definition?: any,
|
definition?: any
|
||||||
isOfficial: boolean
|
isOfficial: boolean
|
||||||
} | undefined {
|
}
|
||||||
|
| 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 === "") {
|
||||||
|
@ -173,20 +176,23 @@ export default class UserRelatedState extends ElementsState {
|
||||||
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,13 +226,14 @@ 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
|
||||||
|
@ -245,27 +244,27 @@ export class And extends TagsFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -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,11 +28,11 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,23 +45,23 @@ 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) {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue