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"
|
||||
steps:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v1.4.6
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: "16"
|
||||
cache: "npm"
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: install deps
|
||||
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
|
||||
on:
|
||||
push
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -28,13 +27,15 @@ jobs:
|
|||
cd pietervdvn.github.io
|
||||
git pull
|
||||
|
||||
- name: get branch name
|
||||
run: echo TARGET_BRANCH=${GITHUB_REF:11} >> $GITHUB_ENV
|
||||
|
||||
- name: "Copying files"
|
||||
run: |
|
||||
echo "Deploying"
|
||||
TARGET=${GITHUB_REF:11}
|
||||
rm -rf pietervdvn.github.io/mc/$TARGET/*
|
||||
mkdir -p pietervdvn.github.io/mc/$TARGET/
|
||||
cp -r dist/* pietervdvn.github.io/mc/$TARGET/
|
||||
rm -rf pietervdvn.github.io/mc/${{ env.TARGET_BRANCH }}/*
|
||||
mkdir -p pietervdvn.github.io/mc/${{ env.TARGET_BRANCH }}/
|
||||
cp -r dist/* pietervdvn.github.io/mc/${{ env.TARGET_BRANCH }}/
|
||||
cd pietervdvn.github.io/
|
||||
git add *
|
||||
if git status | grep -q "Changes to be committed"
|
||||
|
@ -44,3 +45,13 @@ jobs:
|
|||
else
|
||||
echo "No changes to commit"
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup and validate themes
|
||||
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 LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import BaseUIElement from "../UI/BaseUIElement";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import Title from "../UI/Base/Title";
|
||||
import List from "../UI/Base/List";
|
||||
import DependencyCalculator from "../Models/ThemeConfig/DependencyCalculator";
|
||||
import Constants from "../Models/Constants";
|
||||
import {Utils} from "../Utils";
|
||||
import Link from "../UI/Base/Link";
|
||||
import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
|
||||
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import BaseUIElement from "../UI/BaseUIElement"
|
||||
import Combine from "../UI/Base/Combine"
|
||||
import Title from "../UI/Base/Title"
|
||||
import List from "../UI/Base/List"
|
||||
import DependencyCalculator from "../Models/ThemeConfig/DependencyCalculator"
|
||||
import Constants from "../Models/Constants"
|
||||
import { Utils } from "../Utils"
|
||||
import Link from "../UI/Base/Link"
|
||||
import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson"
|
||||
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
|
||||
export class AllKnownLayouts {
|
||||
public static allKnownLayouts: Map<string, LayoutConfig> = AllKnownLayouts.AllLayouts();
|
||||
public static layoutsList: LayoutConfig[] = AllKnownLayouts.GenerateOrderedList(AllKnownLayouts.allKnownLayouts);
|
||||
public static allKnownLayouts: Map<string, LayoutConfig> = AllKnownLayouts.AllLayouts()
|
||||
public static layoutsList: LayoutConfig[] = AllKnownLayouts.GenerateOrderedList(
|
||||
AllKnownLayouts.allKnownLayouts
|
||||
)
|
||||
// Must be below the list...
|
||||
private static sharedLayers: Map<string, LayerConfig> = AllKnownLayouts.getSharedLayers();
|
||||
private static sharedLayers: Map<string, LayerConfig> = AllKnownLayouts.getSharedLayers()
|
||||
|
||||
public static AllPublicLayers(options?: {
|
||||
includeInlineLayers:true | boolean
|
||||
}) : LayerConfig[] {
|
||||
includeInlineLayers: true | boolean
|
||||
}): LayerConfig[] {
|
||||
const allLayers: LayerConfig[] = []
|
||||
const seendIds = new Set<string>()
|
||||
AllKnownLayouts.sharedLayers.forEach((layer, key) => {
|
||||
|
@ -28,7 +30,7 @@ export class AllKnownLayouts {
|
|||
allLayers.push(layer)
|
||||
})
|
||||
if (options?.includeInlineLayers ?? true) {
|
||||
const publicLayouts = AllKnownLayouts.layoutsList.filter(l => !l.hideFromOverview)
|
||||
const publicLayouts = AllKnownLayouts.layoutsList.filter((l) => !l.hideFromOverview)
|
||||
for (const layout of publicLayouts) {
|
||||
if (layout.hideFromOverview) {
|
||||
continue
|
||||
|
@ -40,7 +42,6 @@ export class AllKnownLayouts {
|
|||
seendIds.add(layer.id)
|
||||
allLayers.push(layer)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,11 +53,14 @@ export class AllKnownLayouts {
|
|||
*/
|
||||
public static themesUsingLayer(id: string, publicOnly = true): LayoutConfig[] {
|
||||
const themes = AllKnownLayouts.layoutsList
|
||||
.filter(l => !(publicOnly && l.hideFromOverview) && l.id !== "personal")
|
||||
.map(theme => ({theme, minzoom: theme.layers.find(layer => layer.id === id)?.minzoom}))
|
||||
.filter(obj => obj.minzoom !== undefined)
|
||||
.filter((l) => !(publicOnly && l.hideFromOverview) && l.id !== "personal")
|
||||
.map((theme) => ({
|
||||
theme,
|
||||
minzoom: theme.layers.find((layer) => layer.id === id)?.minzoom,
|
||||
}))
|
||||
.filter((obj) => obj.minzoom !== undefined)
|
||||
themes.sort((th0, th1) => th1.minzoom - th0.minzoom)
|
||||
return themes.map(th => th.theme);
|
||||
return themes.map((th) => th.theme)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,12 +69,15 @@ export class AllKnownLayouts {
|
|||
* @param callback
|
||||
* @constructor
|
||||
*/
|
||||
public static GenOverviewsForSingleLayer(callback: (layer: LayerConfig, element: BaseUIElement, inlineSource: string) => void): void {
|
||||
const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values())
|
||||
.filter(layer => Constants.priviliged_layers.indexOf(layer.id) < 0)
|
||||
public static GenOverviewsForSingleLayer(
|
||||
callback: (layer: LayerConfig, element: BaseUIElement, inlineSource: string) => void
|
||||
): void {
|
||||
const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()).filter(
|
||||
(layer) => Constants.priviliged_layers.indexOf(layer.id) < 0
|
||||
)
|
||||
const builtinLayerIds: Set<string> = new Set<string>()
|
||||
allLayers.forEach(l => builtinLayerIds.add(l.id))
|
||||
const inlineLayers = new Map<string, string>();
|
||||
allLayers.forEach((l) => builtinLayerIds.add(l.id))
|
||||
const inlineLayers = new Map<string, string>()
|
||||
|
||||
for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
|
||||
if (layout.hideFromOverview) {
|
||||
|
@ -78,7 +85,6 @@ export class AllKnownLayouts {
|
|||
}
|
||||
|
||||
for (const layer of layout.layers) {
|
||||
|
||||
if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
|
||||
continue
|
||||
}
|
||||
|
@ -113,7 +119,6 @@ export class AllKnownLayouts {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Determine the cross-dependencies
|
||||
const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>()
|
||||
|
||||
|
@ -125,12 +130,14 @@ export class AllKnownLayouts {
|
|||
}
|
||||
layerIsNeededBy.get(dependency).push(layer.id)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
allLayers.forEach((layer) => {
|
||||
const element = layer.GenerateDocumentation(themesPerLayer.get(layer.id), layerIsNeededBy, DependencyCalculator.getLayerDependencies(layer))
|
||||
const element = layer.GenerateDocumentation(
|
||||
themesPerLayer.get(layer.id),
|
||||
layerIsNeededBy,
|
||||
DependencyCalculator.getLayerDependencies(layer)
|
||||
)
|
||||
callback(layer, element, inlineLayers.get(layer.id))
|
||||
})
|
||||
}
|
||||
|
@ -146,11 +153,12 @@ export class AllKnownLayouts {
|
|||
}
|
||||
}
|
||||
|
||||
const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values())
|
||||
.filter(layer => Constants.priviliged_layers.indexOf(layer.id) < 0)
|
||||
const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()).filter(
|
||||
(layer) => Constants.priviliged_layers.indexOf(layer.id) < 0
|
||||
)
|
||||
|
||||
const builtinLayerIds: Set<string> = new Set<string>()
|
||||
allLayers.forEach(l => builtinLayerIds.add(l.id))
|
||||
allLayers.forEach((l) => builtinLayerIds.add(l.id))
|
||||
|
||||
const themesPerLayer = new Map<string, string[]>()
|
||||
|
||||
|
@ -166,7 +174,6 @@ export class AllKnownLayouts {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Determine the cross-dependencies
|
||||
const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>()
|
||||
|
||||
|
@ -178,25 +185,32 @@ export class AllKnownLayouts {
|
|||
}
|
||||
layerIsNeededBy.get(dependency).push(layer.id)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
return new Combine([
|
||||
new Title("Special and other useful layers", 1),
|
||||
"MapComplete has a few data layers available in the theme which have special properties through builtin-hooks. Furthermore, there are some normal layers (which are built from normal Theme-config files) but are so general that they get a mention here.",
|
||||
new Title("Priviliged layers", 1),
|
||||
new List(Constants.priviliged_layers.map(id => "[" + id + "](#" + id + ")")),
|
||||
new List(Constants.priviliged_layers.map((id) => "[" + id + "](#" + id + ")")),
|
||||
...Constants.priviliged_layers
|
||||
.map(id => AllKnownLayouts.sharedLayers.get(id))
|
||||
.map((l) => l.GenerateDocumentation(themesPerLayer.get(l.id), layerIsNeededBy, DependencyCalculator.getLayerDependencies(l), Constants.added_by_default.indexOf(l.id) >= 0, Constants.no_include.indexOf(l.id) < 0)),
|
||||
.map((id) => AllKnownLayouts.sharedLayers.get(id))
|
||||
.map((l) =>
|
||||
l.GenerateDocumentation(
|
||||
themesPerLayer.get(l.id),
|
||||
layerIsNeededBy,
|
||||
DependencyCalculator.getLayerDependencies(l),
|
||||
Constants.added_by_default.indexOf(l.id) >= 0,
|
||||
Constants.no_include.indexOf(l.id) < 0
|
||||
)
|
||||
),
|
||||
new Title("Normal layers", 1),
|
||||
"The following layers are included in MapComplete:",
|
||||
new List(Array.from(AllKnownLayouts.sharedLayers.keys()).map(id => new Link(id, "./Layers/" + id + ".md")))
|
||||
new List(
|
||||
Array.from(AllKnownLayouts.sharedLayers.keys()).map(
|
||||
(id) => new Link(id, "./Layers/" + id + ".md")
|
||||
)
|
||||
),
|
||||
])
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static GenerateDocumentationForTheme(theme: LayoutConfig): BaseUIElement {
|
||||
|
@ -204,37 +218,42 @@ export class AllKnownLayouts {
|
|||
new Title(new Combine([theme.title, "(", theme.id + ")"]), 2),
|
||||
theme.description,
|
||||
"This theme contains the following layers:",
|
||||
new List(theme.layers.map(l => l.id)),
|
||||
new List(theme.layers.map((l) => l.id)),
|
||||
"Available languages:",
|
||||
new List(theme.language)
|
||||
new List(theme.language),
|
||||
])
|
||||
}
|
||||
|
||||
public static getSharedLayers(): Map<string, LayerConfig> {
|
||||
const sharedLayers = new Map<string, LayerConfig>();
|
||||
const sharedLayers = new Map<string, LayerConfig>()
|
||||
for (const layer of known_themes["layers"]) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const parsed = new LayerConfig(layer, "shared_layers")
|
||||
sharedLayers.set(layer.id, parsed);
|
||||
sharedLayers.set(layer.id, parsed)
|
||||
} catch (e) {
|
||||
if (!Utils.runningFromConsole) {
|
||||
console.error("CRITICAL: Could not parse a layer configuration!", layer.id, " due to", e)
|
||||
console.error(
|
||||
"CRITICAL: Could not parse a layer configuration!",
|
||||
layer.id,
|
||||
" due to",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sharedLayers;
|
||||
return sharedLayers
|
||||
}
|
||||
|
||||
public static getSharedLayersConfigs(): Map<string, LayerConfigJson> {
|
||||
const sharedLayers = new Map<string, LayerConfigJson>();
|
||||
const sharedLayers = new Map<string, LayerConfigJson>()
|
||||
for (const layer of known_themes["layers"]) {
|
||||
// @ts-ignore
|
||||
sharedLayers.set(layer.id, layer);
|
||||
// @ts-ignore
|
||||
sharedLayers.set(layer.id, layer)
|
||||
}
|
||||
|
||||
return sharedLayers;
|
||||
return sharedLayers
|
||||
}
|
||||
|
||||
private static GenerateOrderedList(allKnownLayouts: Map<string, LayoutConfig>): LayoutConfig[] {
|
||||
|
@ -242,28 +261,26 @@ export class AllKnownLayouts {
|
|||
allKnownLayouts.forEach((layout) => {
|
||||
list.push(layout)
|
||||
})
|
||||
return list;
|
||||
return list
|
||||
}
|
||||
|
||||
private static AllLayouts(): Map<string, LayoutConfig> {
|
||||
const dict: Map<string, LayoutConfig> = new Map();
|
||||
const dict: Map<string, LayoutConfig> = new Map()
|
||||
for (const layoutConfigJson of known_themes["themes"]) {
|
||||
const layout = new LayoutConfig(<LayoutConfigJson>layoutConfigJson, true)
|
||||
dict.set(layout.id, layout)
|
||||
for (let i = 0; i < layout.layers.length; i++) {
|
||||
let layer = layout.layers[i];
|
||||
if (typeof (layer) === "string") {
|
||||
layer = AllKnownLayouts.sharedLayers.get(layer);
|
||||
let layer = layout.layers[i]
|
||||
if (typeof layer === "string") {
|
||||
layer = AllKnownLayouts.sharedLayers.get(layer)
|
||||
layout.layers[i] = layer
|
||||
if (layer === undefined) {
|
||||
console.log("Defined layers are ", AllKnownLayouts.sharedLayers.keys())
|
||||
throw `Layer ${layer} was not found or defined - probably a type was made`
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return dict;
|
||||
return dict
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,38 +1,49 @@
|
|||
import * as questions from "../assets/tagRenderings/questions.json";
|
||||
import * as icons from "../assets/tagRenderings/icons.json";
|
||||
import {Utils} from "../Utils";
|
||||
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
|
||||
import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson";
|
||||
import BaseUIElement from "../UI/BaseUIElement";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import Title from "../UI/Base/Title";
|
||||
import {FixedUiElement} from "../UI/Base/FixedUiElement";
|
||||
import List from "../UI/Base/List";
|
||||
import * as questions from "../assets/tagRenderings/questions.json"
|
||||
import * as icons from "../assets/tagRenderings/icons.json"
|
||||
import { Utils } from "../Utils"
|
||||
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
|
||||
import BaseUIElement from "../UI/BaseUIElement"
|
||||
import Combine from "../UI/Base/Combine"
|
||||
import Title from "../UI/Base/Title"
|
||||
import { FixedUiElement } from "../UI/Base/FixedUiElement"
|
||||
import List from "../UI/Base/List"
|
||||
|
||||
export default class SharedTagRenderings {
|
||||
|
||||
public static SharedTagRendering: Map<string, TagRenderingConfig> = SharedTagRenderings.generatedSharedFields();
|
||||
public static SharedTagRenderingJson: Map<string, TagRenderingConfigJson> = SharedTagRenderings.generatedSharedFieldsJsons();
|
||||
public static SharedIcons: Map<string, TagRenderingConfig> = SharedTagRenderings.generatedSharedFields(true);
|
||||
public static SharedTagRendering: Map<string, TagRenderingConfig> =
|
||||
SharedTagRenderings.generatedSharedFields()
|
||||
public static SharedTagRenderingJson: Map<string, TagRenderingConfigJson> =
|
||||
SharedTagRenderings.generatedSharedFieldsJsons()
|
||||
public static SharedIcons: Map<string, TagRenderingConfig> =
|
||||
SharedTagRenderings.generatedSharedFields(true)
|
||||
|
||||
private static generatedSharedFields(iconsOnly = false): Map<string, TagRenderingConfig> {
|
||||
const configJsons = SharedTagRenderings.generatedSharedFieldsJsons(iconsOnly)
|
||||
const d = new Map<string, TagRenderingConfig>()
|
||||
for (const key of Array.from(configJsons.keys())) {
|
||||
try {
|
||||
d.set(key, new TagRenderingConfig(configJsons.get(key), `SharedTagRenderings.${key}`))
|
||||
d.set(
|
||||
key,
|
||||
new TagRenderingConfig(configJsons.get(key), `SharedTagRenderings.${key}`)
|
||||
)
|
||||
} catch (e) {
|
||||
if (!Utils.runningFromConsole) {
|
||||
console.error("BUG: could not parse", key, " from questions.json or icons.json - this error happened during the build step of the SharedTagRenderings", e)
|
||||
|
||||
console.error(
|
||||
"BUG: could not parse",
|
||||
key,
|
||||
" from questions.json or icons.json - this error happened during the build step of the SharedTagRenderings",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
private static generatedSharedFieldsJsons(iconsOnly = false): Map<string, TagRenderingConfigJson> {
|
||||
const dict = new Map<string, TagRenderingConfigJson>();
|
||||
private static generatedSharedFieldsJsons(
|
||||
iconsOnly = false
|
||||
): Map<string, TagRenderingConfigJson> {
|
||||
const dict = new Map<string, TagRenderingConfigJson>()
|
||||
|
||||
if (!iconsOnly) {
|
||||
for (const key in questions) {
|
||||
|
@ -53,13 +64,16 @@ export default class SharedTagRenderings {
|
|||
if (key === "id") {
|
||||
return
|
||||
}
|
||||
value.id = value.id ?? key;
|
||||
if(value["builtin"] !== undefined){
|
||||
if(value["override"] == undefined){
|
||||
throw "HUH? Why whould you want to reuse a builtin if one doesn't override? In questions.json/"+key
|
||||
value.id = value.id ?? key
|
||||
if (value["builtin"] !== undefined) {
|
||||
if (value["override"] == undefined) {
|
||||
throw (
|
||||
"HUH? Why whould you want to reuse a builtin if one doesn't override? In questions.json/" +
|
||||
key
|
||||
)
|
||||
}
|
||||
if(typeof value["builtin"] !== "string"){
|
||||
return;
|
||||
if (typeof value["builtin"] !== "string") {
|
||||
return
|
||||
}
|
||||
// This is a really funny situation: we extend another tagRendering!
|
||||
const parent = Utils.Clone(dict.get(value["builtin"]))
|
||||
|
@ -73,36 +87,31 @@ export default class SharedTagRenderings {
|
|||
}
|
||||
})
|
||||
|
||||
|
||||
return dict;
|
||||
return dict
|
||||
}
|
||||
|
||||
|
||||
public static HelpText(): BaseUIElement {
|
||||
return new Combine([
|
||||
new Combine([
|
||||
new Title("Builtin questions", 1),
|
||||
|
||||
new Title("Builtin questions",1),
|
||||
|
||||
"The following items can be easily reused in your layers"
|
||||
"The following items can be easily reused in your layers",
|
||||
]).SetClass("flex flex-col"),
|
||||
|
||||
... Array.from( SharedTagRenderings.SharedTagRendering.keys()).map(key => {
|
||||
...Array.from(SharedTagRenderings.SharedTagRendering.keys()).map((key) => {
|
||||
const tr = SharedTagRenderings.SharedTagRendering.get(key)
|
||||
let mappings: BaseUIElement = undefined
|
||||
if(tr.mappings?.length > 0){
|
||||
mappings = new List(tr.mappings.map(m => m.then.textFor("en")))
|
||||
if (tr.mappings?.length > 0) {
|
||||
mappings = new List(tr.mappings.map((m) => m.then.textFor("en")))
|
||||
}
|
||||
return new Combine([
|
||||
new Title(key),
|
||||
tr.render?.textFor("en"),
|
||||
tr.question?.textFor("en") ?? new FixedUiElement("Read-only tagrendering").SetClass("font-bold"),
|
||||
mappings
|
||||
tr.question?.textFor("en") ??
|
||||
new FixedUiElement("Read-only tagrendering").SetClass("font-bold"),
|
||||
mappings,
|
||||
]).SetClass("flex flex-col")
|
||||
|
||||
})
|
||||
|
||||
}),
|
||||
]).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,29 +1,27 @@
|
|||
import {existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync} from "fs";
|
||||
import ScriptUtils from "../../scripts/ScriptUtils";
|
||||
import {Utils} from "../../Utils";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs"
|
||||
import ScriptUtils from "../../scripts/ScriptUtils"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
ScriptUtils.fixUtils()
|
||||
|
||||
class StatsDownloader {
|
||||
private readonly urlTemplate =
|
||||
"https://osmcha.org/api/v1/changesets/?date__gte={start_date}&date__lte={end_date}&page={page}&comment=%23mapcomplete&page_size=100"
|
||||
|
||||
private readonly urlTemplate = "https://osmcha.org/api/v1/changesets/?date__gte={start_date}&date__lte={end_date}&page={page}&comment=%23mapcomplete&page_size=100"
|
||||
|
||||
private readonly _targetDirectory: string;
|
||||
private readonly _targetDirectory: string
|
||||
|
||||
constructor(targetDirectory = ".") {
|
||||
this._targetDirectory = targetDirectory;
|
||||
this._targetDirectory = targetDirectory
|
||||
}
|
||||
|
||||
public async DownloadStats(startYear = 2020, startMonth = 5) {
|
||||
|
||||
const today = new Date();
|
||||
public async DownloadStats(startYear = 2020, startMonth = 5, startDay = 1) {
|
||||
const today = new Date()
|
||||
const currentYear = today.getFullYear()
|
||||
const currentMonth = today.getMonth() + 1
|
||||
for (let year = startYear; year <= currentYear; year++) {
|
||||
for (let month = 1; month <= 12; month++) {
|
||||
|
||||
if (year === startYear && month < startMonth) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
if (year === currentYear && month > currentMonth) {
|
||||
|
@ -32,26 +30,40 @@ class StatsDownloader {
|
|||
|
||||
const pathM = `${this._targetDirectory}/stats.${year}-${month}.json`
|
||||
if (existsSync(pathM)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
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()) {
|
||||
break;
|
||||
monthIsFinished = false
|
||||
break
|
||||
}
|
||||
{
|
||||
const date = new Date(year, month - 1, day)
|
||||
if(date.getMonth() != month -1){
|
||||
if (date.getMonth() != month - 1) {
|
||||
// We did roll over
|
||||
continue
|
||||
}
|
||||
}
|
||||
const path = `${this._targetDirectory}/stats.${year}-${month}-${(day < 10 ? "0" : "") + day}.day.json`
|
||||
const path = `${this._targetDirectory}/stats.${year}-${month}-${
|
||||
(day < 10 ? "0" : "") + day
|
||||
}.day.json`
|
||||
writtenFiles.push(path)
|
||||
if (existsSync(path)) {
|
||||
features.push(...JSON.parse(readFileSync(path, "UTF-8")))
|
||||
console.log("Loaded ", path, "from disk, got", features.length, "features now")
|
||||
let features = JSON.parse(readFileSync(path, "UTF-8"))
|
||||
features = features?.features ?? features
|
||||
console.log(features)
|
||||
features.push(...features.features) // day-stats are generally a list already, but in some ad-hoc cases might be a geojson-collection too
|
||||
console.log(
|
||||
"Loaded ",
|
||||
path,
|
||||
"from disk, got",
|
||||
features.length,
|
||||
"features now"
|
||||
)
|
||||
continue
|
||||
}
|
||||
let dayFeatures: any[] = undefined
|
||||
|
@ -59,47 +71,72 @@ class StatsDownloader {
|
|||
dayFeatures = await this.DownloadStatsForDay(year, month, day, path)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error("Could not download " + year + "-" + month + "-" + day + "... Trying again")
|
||||
console.error(
|
||||
"Could not download " +
|
||||
year +
|
||||
"-" +
|
||||
month +
|
||||
"-" +
|
||||
day +
|
||||
"... Trying again"
|
||||
)
|
||||
dayFeatures = await this.DownloadStatsForDay(year, month, day, path)
|
||||
}
|
||||
writeFileSync(path, JSON.stringify(dayFeatures))
|
||||
features.push(...dayFeatures)
|
||||
|
||||
}
|
||||
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, month: number, day: number, path: string): Promise<any[]> {
|
||||
|
||||
let page = 1;
|
||||
public async DownloadStatsForDay(
|
||||
year: number,
|
||||
month: number,
|
||||
day: number,
|
||||
path: string
|
||||
): Promise<any[]> {
|
||||
let page = 1
|
||||
let allFeatures = []
|
||||
let endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1);
|
||||
let endDate = `${endDay.getFullYear()}-${Utils.TwoDigits(endDay.getMonth() + 1)}-${Utils.TwoDigits(endDay.getDate())}`
|
||||
let url = this.urlTemplate.replace("{start_date}", year + "-" + Utils.TwoDigits(month) + "-" + Utils.TwoDigits(day))
|
||||
let endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1)
|
||||
let endDate = `${endDay.getFullYear()}-${Utils.TwoDigits(
|
||||
endDay.getMonth() + 1
|
||||
)}-${Utils.TwoDigits(endDay.getDate())}`
|
||||
let url = this.urlTemplate
|
||||
.replace(
|
||||
"{start_date}",
|
||||
year + "-" + Utils.TwoDigits(month) + "-" + Utils.TwoDigits(day)
|
||||
)
|
||||
.replace("{end_date}", endDate)
|
||||
.replace("{page}", "" + page)
|
||||
|
||||
|
||||
let headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Referer': 'https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Token 6e422e2afedb79ef66573982012000281f03dc91',
|
||||
'DNT': '1',
|
||||
'Connection': 'keep-alive',
|
||||
'TE': 'Trailers',
|
||||
'Pragma': 'no-cache',
|
||||
'Cache-Control': 'no-cache'
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
Referer:
|
||||
"https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D",
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Token 6e422e2afedb79ef66573982012000281f03dc91",
|
||||
DNT: "1",
|
||||
Connection: "keep-alive",
|
||||
TE: "Trailers",
|
||||
Pragma: "no-cache",
|
||||
"Cache-Control": "no-cache",
|
||||
}
|
||||
|
||||
while (url) {
|
||||
ScriptUtils.erasableLog(`Downloading stats for ${year}-${month}-${day}, page ${page} ${url}`)
|
||||
ScriptUtils.erasableLog(
|
||||
`Downloading stats for ${year}-${month}-${day}, page ${page} ${url}`
|
||||
)
|
||||
const result = await Utils.downloadJson(url, headers)
|
||||
page++;
|
||||
page++
|
||||
allFeatures.push(...result.features)
|
||||
if (result.features === undefined) {
|
||||
console.log("ERROR", result)
|
||||
|
@ -107,58 +144,59 @@ class StatsDownloader {
|
|||
}
|
||||
url = result.next
|
||||
}
|
||||
console.log(`Writing ${allFeatures.length} features to `, path, Utils.Times(_ => " ", 80))
|
||||
console.log(
|
||||
`Writing ${allFeatures.length} features to `,
|
||||
path,
|
||||
Utils.Times((_) => " ", 80)
|
||||
)
|
||||
allFeatures = Utils.NoNull(allFeatures)
|
||||
allFeatures.forEach(f => {
|
||||
f.properties = {...f.properties, ...f.properties.metadata}
|
||||
allFeatures.forEach((f) => {
|
||||
f.properties = { ...f.properties, ...f.properties.metadata }
|
||||
delete f.properties.metadata
|
||||
f.properties.id = f.id
|
||||
})
|
||||
return allFeatures
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
interface ChangeSetData {
|
||||
"id": number,
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [number, number][][]
|
||||
},
|
||||
"properties": {
|
||||
"check_user": null,
|
||||
"reasons": [],
|
||||
"tags": [],
|
||||
"features": [],
|
||||
"user": string,
|
||||
"uid": string,
|
||||
"editor": string,
|
||||
"comment": string,
|
||||
"comments_count": number,
|
||||
"source": string,
|
||||
"imagery_used": string,
|
||||
"date": string,
|
||||
"reviewed_features": [],
|
||||
"create": number,
|
||||
"modify": number,
|
||||
"delete": number,
|
||||
"area": number,
|
||||
"is_suspect": boolean,
|
||||
"harmful": any,
|
||||
"checked": boolean,
|
||||
"check_date": any,
|
||||
"metadata": {
|
||||
"host": string,
|
||||
"theme": string,
|
||||
"imagery": string,
|
||||
"language": string
|
||||
id: number
|
||||
type: "Feature"
|
||||
geometry: {
|
||||
type: "Polygon"
|
||||
coordinates: [number, number][][]
|
||||
}
|
||||
properties: {
|
||||
check_user: null
|
||||
reasons: []
|
||||
tags: []
|
||||
features: []
|
||||
user: string
|
||||
uid: string
|
||||
editor: string
|
||||
comment: string
|
||||
comments_count: number
|
||||
source: string
|
||||
imagery_used: string
|
||||
date: string
|
||||
reviewed_features: []
|
||||
create: number
|
||||
modify: number
|
||||
delete: number
|
||||
area: number
|
||||
is_suspect: boolean
|
||||
harmful: any
|
||||
checked: boolean
|
||||
check_date: any
|
||||
metadata: {
|
||||
host: string
|
||||
theme: string
|
||||
imagery: string
|
||||
language: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function main(): Promise<void> {
|
||||
if (!existsSync("graphs")) {
|
||||
mkdirSync("graphs")
|
||||
|
@ -167,38 +205,48 @@ async function main(): Promise<void> {
|
|||
const targetDir = "Docs/Tools/stats"
|
||||
let year = 2020
|
||||
let month = 5
|
||||
if(!isNaN(Number(process.argv[2]))){
|
||||
let day = 1
|
||||
if (!isNaN(Number(process.argv[2]))) {
|
||||
year = Number(process.argv[2])
|
||||
}
|
||||
if(!isNaN(Number(process.argv[3]))){
|
||||
if (!isNaN(Number(process.argv[3]))) {
|
||||
month = Number(process.argv[3])
|
||||
}
|
||||
|
||||
|
||||
if (!isNaN(Number(process.argv[4]))) {
|
||||
day = Number(process.argv[4])
|
||||
}
|
||||
|
||||
do {
|
||||
try {
|
||||
|
||||
await new StatsDownloader(targetDir).DownloadStats(year, month)
|
||||
await new StatsDownloader(targetDir).DownloadStats(year, month, day)
|
||||
break
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
} while (true)
|
||||
const allPaths = readdirSync(targetDir)
|
||||
.filter(p => p.startsWith("stats.") && p.endsWith(".json"));
|
||||
let allFeatures: ChangeSetData[] = [].concat(...allPaths
|
||||
.map(path => JSON.parse(readFileSync("Docs/Tools/stats/" + path, "utf-8")).features));
|
||||
allFeatures = allFeatures.filter(f => f?.properties !== undefined && (f.properties.editor === null || f.properties.editor.toLowerCase().startsWith("mapcomplete")))
|
||||
const allPaths = readdirSync(targetDir).filter(
|
||||
(p) => p.startsWith("stats.") && p.endsWith(".json")
|
||||
)
|
||||
let allFeatures: ChangeSetData[] = [].concat(
|
||||
...allPaths.map(
|
||||
(path) => JSON.parse(readFileSync("Docs/Tools/stats/" + path, "utf-8")).features
|
||||
)
|
||||
)
|
||||
allFeatures = allFeatures.filter(
|
||||
(f) =>
|
||||
f?.properties !== undefined &&
|
||||
(f.properties.editor === null ||
|
||||
f.properties.editor.toLowerCase().startsWith("mapcomplete"))
|
||||
)
|
||||
|
||||
allFeatures = allFeatures.filter(f => f.properties.metadata?.theme !== "EMPTY CS")
|
||||
allFeatures = allFeatures.filter((f) => f.properties.metadata?.theme !== "EMPTY CS")
|
||||
|
||||
if (process.argv.indexOf("--no-graphs") >= 0) {
|
||||
return
|
||||
}
|
||||
const allFiles = readdirSync("Docs/Tools/stats").filter(p => p.endsWith(".json"))
|
||||
const allFiles = readdirSync("Docs/Tools/stats").filter((p) => p.endsWith(".json"))
|
||||
writeFileSync("Docs/Tools/stats/file-overview.json", JSON.stringify(allFiles))
|
||||
|
||||
}
|
||||
|
||||
main().then(_ => console.log("All done!"))
|
||||
|
||||
main().then((_) => console.log("All done!"))
|
||||
|
|
|
@ -1,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"
|
||||
]
|
||||
["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 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 {ImmutableStore, Store, UIEventSource} from "../UIEventSource";
|
||||
import Loc from "../../Models/Loc";
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
|
||||
export interface AvailableBaseLayersObj {
|
||||
readonly osmCarto: BaseLayer;
|
||||
layerOverview: BaseLayer[];
|
||||
readonly osmCarto: BaseLayer
|
||||
layerOverview: BaseLayer[]
|
||||
|
||||
AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]>
|
||||
|
||||
SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: Store<string | string[]>): Store<BaseLayer>;
|
||||
|
||||
SelectBestLayerAccordingTo(
|
||||
location: Store<Loc>,
|
||||
preferedCategory: Store<string | string[]>
|
||||
): Store<BaseLayer>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,20 +19,28 @@ export interface AvailableBaseLayersObj {
|
|||
* Changes the basemap
|
||||
*/
|
||||
export default class AvailableBaseLayers {
|
||||
|
||||
|
||||
public static layerOverview: BaseLayer[];
|
||||
public static osmCarto: BaseLayer;
|
||||
public static layerOverview: BaseLayer[]
|
||||
public static osmCarto: BaseLayer
|
||||
|
||||
private static implementation: AvailableBaseLayersObj
|
||||
|
||||
static AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> {
|
||||
return AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? new ImmutableStore<BaseLayer[]>([]);
|
||||
return (
|
||||
AvailableBaseLayers.implementation?.AvailableLayersAt(location) ??
|
||||
new ImmutableStore<BaseLayer[]>([])
|
||||
)
|
||||
}
|
||||
|
||||
static SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: UIEventSource<string | string[]>): Store<BaseLayer> {
|
||||
return AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(location, preferedCategory) ?? new ImmutableStore<BaseLayer>(undefined);
|
||||
|
||||
static SelectBestLayerAccordingTo(
|
||||
location: Store<Loc>,
|
||||
preferedCategory: UIEventSource<string | string[]>
|
||||
): Store<BaseLayer> {
|
||||
return (
|
||||
AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(
|
||||
location,
|
||||
preferedCategory
|
||||
) ?? new ImmutableStore<BaseLayer>(undefined)
|
||||
)
|
||||
}
|
||||
|
||||
public static implement(backend: AvailableBaseLayersObj) {
|
||||
|
@ -38,5 +48,4 @@ export default class AvailableBaseLayers {
|
|||
AvailableBaseLayers.osmCarto = backend.osmCarto
|
||||
AvailableBaseLayers.implementation = backend
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,66 +1,77 @@
|
|||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import {Store, Stores} from "../UIEventSource";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {GeoOperations} from "../GeoOperations";
|
||||
import * as editorlayerindex from "../../assets/editor-layer-index.json";
|
||||
import * as L from "leaflet";
|
||||
import {TileLayer} from "leaflet";
|
||||
import * as X from "leaflet-providers";
|
||||
import {Utils} from "../../Utils";
|
||||
import {AvailableBaseLayersObj} from "./AvailableBaseLayers";
|
||||
import {BBox} from "../BBox";
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import { Store, Stores } from "../UIEventSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import * as editorlayerindex from "../../assets/editor-layer-index.json"
|
||||
import * as L from "leaflet"
|
||||
import { TileLayer } from "leaflet"
|
||||
import * as X from "leaflet-providers"
|
||||
import { Utils } from "../../Utils"
|
||||
import { AvailableBaseLayersObj } from "./AvailableBaseLayers"
|
||||
import { BBox } from "../BBox"
|
||||
|
||||
export default class AvailableBaseLayersImplementation implements AvailableBaseLayersObj {
|
||||
|
||||
public readonly osmCarto: BaseLayer =
|
||||
{
|
||||
id: "osm",
|
||||
name: "OpenStreetMap",
|
||||
layer: () => AvailableBaseLayersImplementation.CreateBackgroundLayer("osm", "OpenStreetMap",
|
||||
"https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright",
|
||||
public readonly osmCarto: BaseLayer = {
|
||||
id: "osm",
|
||||
name: "OpenStreetMap",
|
||||
layer: () =>
|
||||
AvailableBaseLayersImplementation.CreateBackgroundLayer(
|
||||
"osm",
|
||||
"OpenStreetMap",
|
||||
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"OpenStreetMap",
|
||||
"https://openStreetMap.org/copyright",
|
||||
19,
|
||||
false, false),
|
||||
feature: null,
|
||||
max_zoom: 19,
|
||||
min_zoom: 0,
|
||||
isBest: true, // Of course, OpenStreetMap is the best map!
|
||||
category: "osmbasedmap"
|
||||
}
|
||||
false,
|
||||
false
|
||||
),
|
||||
feature: null,
|
||||
max_zoom: 19,
|
||||
min_zoom: 0,
|
||||
isBest: true, // Of course, OpenStreetMap is the best map!
|
||||
category: "osmbasedmap",
|
||||
}
|
||||
|
||||
public readonly layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(AvailableBaseLayersImplementation.LoadProviderIndex());
|
||||
public readonly globalLayers = this.layerOverview.filter(layer => layer.feature?.geometry === undefined || layer.feature?.geometry === null)
|
||||
public readonly localLayers = this.layerOverview.filter(layer => layer.feature?.geometry !== undefined && layer.feature?.geometry !== null)
|
||||
public readonly layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(
|
||||
AvailableBaseLayersImplementation.LoadProviderIndex()
|
||||
)
|
||||
public readonly globalLayers = this.layerOverview.filter(
|
||||
(layer) => layer.feature?.geometry === undefined || layer.feature?.geometry === null
|
||||
)
|
||||
public readonly localLayers = this.layerOverview.filter(
|
||||
(layer) => layer.feature?.geometry !== undefined && layer.feature?.geometry !== null
|
||||
)
|
||||
|
||||
private static LoadRasterIndex(): BaseLayer[] {
|
||||
const layers: BaseLayer[] = []
|
||||
// @ts-ignore
|
||||
const features = editorlayerindex.features;
|
||||
const features = editorlayerindex.features
|
||||
for (const i in features) {
|
||||
const layer = features[i];
|
||||
const props = layer.properties;
|
||||
const layer = features[i]
|
||||
const props = layer.properties
|
||||
|
||||
if (props.type === "bing") {
|
||||
// A lot of work to implement - see https://github.com/pietervdvn/MapComplete/issues/648
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
if (props.id === "MAPNIK") {
|
||||
// Already added by default
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
if (props.overlay) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
if (props.url.toLowerCase().indexOf("apikey") > 0) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
if (props.max_zoom < 19) {
|
||||
// We want users to zoom to level 19 when adding a point
|
||||
// If they are on a layer which hasn't enough precision, they can not zoom far enough. This is confusing, so we don't use this layer
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
if (props.name === undefined) {
|
||||
|
@ -68,17 +79,17 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
|
|||
continue
|
||||
}
|
||||
|
||||
|
||||
const leafletLayer: () => TileLayer = () => AvailableBaseLayersImplementation.CreateBackgroundLayer(
|
||||
props.id,
|
||||
props.name,
|
||||
props.url,
|
||||
props.name,
|
||||
props.license_url,
|
||||
props.max_zoom,
|
||||
props.type.toLowerCase() === "wms",
|
||||
props.type.toLowerCase() === "wmts"
|
||||
)
|
||||
const leafletLayer: () => TileLayer = () =>
|
||||
AvailableBaseLayersImplementation.CreateBackgroundLayer(
|
||||
props.id,
|
||||
props.name,
|
||||
props.url,
|
||||
props.name,
|
||||
props.license_url,
|
||||
props.max_zoom,
|
||||
props.type.toLowerCase() === "wms",
|
||||
props.type.toLowerCase() === "wmts"
|
||||
)
|
||||
|
||||
// Note: if layer.geometry is null, there is global coverage for this layer
|
||||
layers.push({
|
||||
|
@ -89,34 +100,35 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
|
|||
layer: leafletLayer,
|
||||
feature: layer.geometry !== null ? layer : null,
|
||||
isBest: props.best ?? false,
|
||||
category: props.category
|
||||
});
|
||||
category: props.category,
|
||||
})
|
||||
}
|
||||
return layers;
|
||||
return layers
|
||||
}
|
||||
|
||||
private static LoadProviderIndex(): BaseLayer[] {
|
||||
// @ts-ignore
|
||||
X; // Import X to make sure the namespace is not optimized away
|
||||
X // Import X to make sure the namespace is not optimized away
|
||||
function l(id: string, name: string): BaseLayer {
|
||||
try {
|
||||
const layer: any = L.tileLayer.provider(id, undefined);
|
||||
const layer: any = L.tileLayer.provider(id, undefined)
|
||||
return {
|
||||
feature: null,
|
||||
id: id,
|
||||
name: name,
|
||||
layer: () => L.tileLayer.provider(id, {
|
||||
maxNativeZoom: layer.options?.maxZoom,
|
||||
maxZoom: Math.max(layer.options?.maxZoom ?? 19, 21)
|
||||
}),
|
||||
layer: () =>
|
||||
L.tileLayer.provider(id, {
|
||||
maxNativeZoom: layer.options?.maxZoom,
|
||||
maxZoom: Math.max(layer.options?.maxZoom ?? 19, 21),
|
||||
}),
|
||||
min_zoom: 1,
|
||||
max_zoom: layer.options.maxZoom,
|
||||
category: "osmbasedmap",
|
||||
isBest: false
|
||||
isBest: false,
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not find provided layer", name, e);
|
||||
return null;
|
||||
console.error("Could not find provided layer", name, e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,38 +141,50 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
|
|||
l("CartoDB.PositronNoLabels", "Positron - no labels (by CartoDB)"),
|
||||
l("CartoDB.Voyager", "Voyager (by CartoDB)"),
|
||||
l("CartoDB.VoyagerNoLabels", "Voyager - no labels (by CartoDB)"),
|
||||
];
|
||||
return Utils.NoNull(layers);
|
||||
|
||||
]
|
||||
return Utils.NoNull(layers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a layer from the editor-layer-index into a tilelayer usable by leaflet
|
||||
*/
|
||||
private static CreateBackgroundLayer(id: string, name: string, url: string, attribution: string, attributionUrl: string,
|
||||
maxZoom: number, isWms: boolean, isWMTS?: boolean): TileLayer {
|
||||
|
||||
url = url.replace("{zoom}", "{z}")
|
||||
.replace("&BBOX={bbox}", "")
|
||||
.replace("&bbox={bbox}", "");
|
||||
private static CreateBackgroundLayer(
|
||||
id: string,
|
||||
name: string,
|
||||
url: string,
|
||||
attribution: string,
|
||||
attributionUrl: string,
|
||||
maxZoom: number,
|
||||
isWms: boolean,
|
||||
isWMTS?: boolean
|
||||
): TileLayer {
|
||||
url = url.replace("{zoom}", "{z}").replace("&BBOX={bbox}", "").replace("&bbox={bbox}", "")
|
||||
|
||||
const subdomainsMatch = url.match(/{switch:[^}]*}/)
|
||||
let domains: string[] = [];
|
||||
let domains: string[] = []
|
||||
if (subdomainsMatch !== null) {
|
||||
let domainsStr = subdomainsMatch[0].substr("{switch:".length);
|
||||
domainsStr = domainsStr.substr(0, domainsStr.length - 1);
|
||||
domains = domainsStr.split(",");
|
||||
let domainsStr = subdomainsMatch[0].substr("{switch:".length)
|
||||
domainsStr = domainsStr.substr(0, domainsStr.length - 1)
|
||||
domains = domainsStr.split(",")
|
||||
url = url.replace(/{switch:[^}]*}/, "{s}")
|
||||
}
|
||||
|
||||
|
||||
if (isWms) {
|
||||
url = url.replace("&SRS={proj}", "");
|
||||
url = url.replace("&srs={proj}", "");
|
||||
const paramaters = ["format", "layers", "version", "service", "request", "styles", "transparent", "version"];
|
||||
const urlObj = new URL(url);
|
||||
url = url.replace("&SRS={proj}", "")
|
||||
url = url.replace("&srs={proj}", "")
|
||||
const paramaters = [
|
||||
"format",
|
||||
"layers",
|
||||
"version",
|
||||
"service",
|
||||
"request",
|
||||
"styles",
|
||||
"transparent",
|
||||
"version",
|
||||
]
|
||||
const urlObj = new URL(url)
|
||||
|
||||
const isUpper = urlObj.searchParams["LAYERS"] !== null;
|
||||
const isUpper = urlObj.searchParams["LAYERS"] !== null
|
||||
const options = {
|
||||
maxZoom: Math.max(maxZoom ?? 19, 21),
|
||||
maxNativeZoom: maxZoom ?? 19,
|
||||
|
@ -168,116 +192,117 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
|
|||
subdomains: domains,
|
||||
uppercase: isUpper,
|
||||
transparent: false,
|
||||
};
|
||||
}
|
||||
|
||||
for (const paramater of paramaters) {
|
||||
let p = paramater;
|
||||
let p = paramater
|
||||
if (isUpper) {
|
||||
p = paramater.toUpperCase();
|
||||
p = paramater.toUpperCase()
|
||||
}
|
||||
options[paramater] = urlObj.searchParams.get(p);
|
||||
options[paramater] = urlObj.searchParams.get(p)
|
||||
}
|
||||
|
||||
if (options.transparent === null) {
|
||||
options.transparent = false;
|
||||
options.transparent = false
|
||||
}
|
||||
|
||||
|
||||
return L.tileLayer.wms(urlObj.protocol + "//" + urlObj.host + urlObj.pathname, options);
|
||||
return L.tileLayer.wms(urlObj.protocol + "//" + urlObj.host + urlObj.pathname, options)
|
||||
}
|
||||
|
||||
if (attributionUrl) {
|
||||
attribution = `<a href='${attributionUrl}' target='_blank'>${attribution}</a>`;
|
||||
attribution = `<a href='${attributionUrl}' target='_blank'>${attribution}</a>`
|
||||
}
|
||||
|
||||
return L.tileLayer(url,
|
||||
{
|
||||
attribution: attribution,
|
||||
maxZoom: Math.max(21, maxZoom ?? 19),
|
||||
maxNativeZoom: maxZoom ?? 19,
|
||||
minZoom: 1,
|
||||
// @ts-ignore
|
||||
wmts: isWMTS ?? false,
|
||||
subdomains: domains
|
||||
});
|
||||
return L.tileLayer(url, {
|
||||
attribution: attribution,
|
||||
maxZoom: Math.max(21, maxZoom ?? 19),
|
||||
maxNativeZoom: maxZoom ?? 19,
|
||||
minZoom: 1,
|
||||
// @ts-ignore
|
||||
wmts: isWMTS ?? false,
|
||||
subdomains: domains,
|
||||
})
|
||||
}
|
||||
|
||||
public AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> {
|
||||
return Stores.ListStabilized(location.map(
|
||||
(currentLocation) => {
|
||||
return Stores.ListStabilized(
|
||||
location.map((currentLocation) => {
|
||||
if (currentLocation === undefined) {
|
||||
return this.layerOverview;
|
||||
return this.layerOverview
|
||||
}
|
||||
return this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
|
||||
}));
|
||||
return this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
public SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: Store<string | string[]>): Store<BaseLayer> {
|
||||
return this.AvailableLayersAt(location)
|
||||
.map(available => {
|
||||
public SelectBestLayerAccordingTo(
|
||||
location: Store<Loc>,
|
||||
preferedCategory: Store<string | string[]>
|
||||
): Store<BaseLayer> {
|
||||
return this.AvailableLayersAt(location).map(
|
||||
(available) => {
|
||||
// First float all 'best layers' to the top
|
||||
available.sort((a, b) => {
|
||||
if (a.isBest && b.isBest) {
|
||||
return 0;
|
||||
}
|
||||
if (!a.isBest) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1;
|
||||
if (a.isBest && b.isBest) {
|
||||
return 0
|
||||
}
|
||||
)
|
||||
if (!a.isBest) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1
|
||||
})
|
||||
|
||||
if (preferedCategory.data === undefined) {
|
||||
return available[0]
|
||||
}
|
||||
|
||||
let prefered: string []
|
||||
let prefered: string[]
|
||||
if (typeof preferedCategory.data === "string") {
|
||||
prefered = [preferedCategory.data]
|
||||
} else {
|
||||
prefered = preferedCategory.data;
|
||||
prefered = preferedCategory.data
|
||||
}
|
||||
|
||||
prefered.reverse(/*New list, inplace reverse is fine*/);
|
||||
prefered.reverse(/*New list, inplace reverse is fine*/)
|
||||
for (const category of prefered) {
|
||||
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
|
||||
available.sort((a, b) => {
|
||||
if (a.category === category && b.category === category) {
|
||||
return 0;
|
||||
}
|
||||
if (a.category !== category) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1;
|
||||
if (a.category === category && b.category === category) {
|
||||
return 0
|
||||
}
|
||||
)
|
||||
if (a.category !== category) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1
|
||||
})
|
||||
}
|
||||
return available[0]
|
||||
}, [preferedCategory])
|
||||
},
|
||||
[preferedCategory]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] {
|
||||
const availableLayers = [this.osmCarto]
|
||||
if (lon === undefined || lat === undefined) {
|
||||
return availableLayers.concat(this.globalLayers);
|
||||
return availableLayers.concat(this.globalLayers)
|
||||
}
|
||||
const lonlat : [number, number] = [lon, lat];
|
||||
const lonlat: [number, number] = [lon, lat]
|
||||
for (const layerOverviewItem of this.localLayers) {
|
||||
const layer = layerOverviewItem;
|
||||
const layer = layerOverviewItem
|
||||
const bbox = BBox.get(layer.feature)
|
||||
|
||||
if(!bbox.contains(lonlat)){
|
||||
|
||||
if (!bbox.contains(lonlat)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (GeoOperations.inside(lonlat, layer.feature)) {
|
||||
availableLayers.push(layer);
|
||||
availableLayers.push(layer)
|
||||
}
|
||||
}
|
||||
|
||||
return availableLayers.concat(this.globalLayers);
|
||||
return availableLayers.concat(this.globalLayers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +1,49 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import AvailableBaseLayers from "./AvailableBaseLayers";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {Utils} from "../../Utils";
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import AvailableBaseLayers from "./AvailableBaseLayers"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
/**
|
||||
* Sets the current background layer to a layer that is actually available
|
||||
*/
|
||||
export default class BackgroundLayerResetter {
|
||||
|
||||
constructor(currentBackgroundLayer: UIEventSource<BaseLayer>,
|
||||
location: UIEventSource<Loc>,
|
||||
availableLayers: UIEventSource<BaseLayer[]>,
|
||||
defaultLayerId: string = undefined) {
|
||||
|
||||
constructor(
|
||||
currentBackgroundLayer: UIEventSource<BaseLayer>,
|
||||
location: UIEventSource<Loc>,
|
||||
availableLayers: UIEventSource<BaseLayer[]>,
|
||||
defaultLayerId: string = undefined
|
||||
) {
|
||||
if (Utils.runningFromConsole) {
|
||||
return
|
||||
}
|
||||
|
||||
defaultLayerId = defaultLayerId ?? AvailableBaseLayers.osmCarto.id;
|
||||
defaultLayerId = defaultLayerId ?? AvailableBaseLayers.osmCarto.id
|
||||
|
||||
// Change the baselayer back to OSM if we go out of the current range of the layer
|
||||
availableLayers.addCallbackAndRun(availableLayers => {
|
||||
let defaultLayer = undefined;
|
||||
const currentLayer = currentBackgroundLayer.data.id;
|
||||
availableLayers.addCallbackAndRun((availableLayers) => {
|
||||
let defaultLayer = undefined
|
||||
const currentLayer = currentBackgroundLayer.data.id
|
||||
for (const availableLayer of availableLayers) {
|
||||
if (availableLayer.id === currentLayer) {
|
||||
|
||||
if (availableLayer.max_zoom < location.data.zoom) {
|
||||
break;
|
||||
break
|
||||
}
|
||||
|
||||
if (availableLayer.min_zoom > location.data.zoom) {
|
||||
break;
|
||||
break
|
||||
}
|
||||
if (availableLayer.id === defaultLayerId) {
|
||||
defaultLayer = availableLayer;
|
||||
defaultLayer = availableLayer
|
||||
}
|
||||
return; // All good - the current layer still works!
|
||||
return // All good - the current layer still works!
|
||||
}
|
||||
}
|
||||
// Oops, we panned out of range for this layer!
|
||||
console.log("AvailableBaseLayers-actor: detected that the current bounds aren't sufficient anymore - reverting to OSM standard")
|
||||
currentBackgroundLayer.setData(defaultLayer ?? AvailableBaseLayers.osmCarto);
|
||||
});
|
||||
|
||||
console.log(
|
||||
"AvailableBaseLayers-actor: detected that the current bounds aren't sufficient anymore - reverting to OSM standard"
|
||||
)
|
||||
currentBackgroundLayer.setData(defaultLayer ?? AvailableBaseLayers.osmCarto)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +1,34 @@
|
|||
import {ElementStorage} from "../ElementStorage";
|
||||
import {Changes} from "../Osm/Changes";
|
||||
import { ElementStorage } from "../ElementStorage"
|
||||
import { Changes } from "../Osm/Changes"
|
||||
|
||||
export default class ChangeToElementsActor {
|
||||
constructor(changes: Changes, allElements: ElementStorage) {
|
||||
changes.pendingChanges.addCallbackAndRun(changes => {
|
||||
changes.pendingChanges.addCallbackAndRun((changes) => {
|
||||
for (const change of changes) {
|
||||
const id = change.type + "/" + change.id;
|
||||
const id = change.type + "/" + change.id
|
||||
if (!allElements.has(id)) {
|
||||
continue; // Ignored as the geometryFixer will introduce this
|
||||
continue // Ignored as the geometryFixer will introduce this
|
||||
}
|
||||
const src = allElements.getEventSourceById(id)
|
||||
|
||||
let changed = false;
|
||||
let changed = false
|
||||
for (const kv of change.tags ?? []) {
|
||||
// Apply tag changes and ping the consumers
|
||||
const k = kv.k
|
||||
let v = kv.v
|
||||
if (v === "") {
|
||||
v = undefined;
|
||||
v = undefined
|
||||
}
|
||||
if (src.data[k] === v) {
|
||||
continue
|
||||
}
|
||||
changed = true;
|
||||
src.data[k] = v;
|
||||
changed = true
|
||||
src.data[k] = v
|
||||
}
|
||||
if (changed) {
|
||||
src.ping()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,60 +1,59 @@
|
|||
import {Store, UIEventSource} from "../UIEventSource";
|
||||
import Svg from "../../Svg";
|
||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {QueryParameters} from "../Web/QueryParameters";
|
||||
import {BBox} from "../BBox";
|
||||
import Constants from "../../Models/Constants";
|
||||
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource";
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import Svg from "../../Svg"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { VariableUiElement } from "../../UI/Base/VariableUIElement"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import { BBox } from "../BBox"
|
||||
import Constants from "../../Models/Constants"
|
||||
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"
|
||||
|
||||
export interface GeoLocationPointProperties {
|
||||
id: "gps",
|
||||
"user:location": "yes",
|
||||
"date": string,
|
||||
"latitude": number
|
||||
"longitude": number,
|
||||
"speed": number,
|
||||
"accuracy": number
|
||||
"heading": number
|
||||
"altitude": number
|
||||
export interface GeoLocationPointProperties {
|
||||
id: "gps"
|
||||
"user:location": "yes"
|
||||
date: string
|
||||
latitude: number
|
||||
longitude: number
|
||||
speed: number
|
||||
accuracy: number
|
||||
heading: number
|
||||
altitude: number
|
||||
}
|
||||
|
||||
export default class GeoLocationHandler extends VariableUiElement {
|
||||
|
||||
private readonly currentLocation?: SimpleFeatureSource
|
||||
|
||||
/**
|
||||
* Wether or not the geolocation is active, aka the user requested the current location
|
||||
*/
|
||||
private readonly _isActive: UIEventSource<boolean>;
|
||||
private readonly _isActive: UIEventSource<boolean>
|
||||
|
||||
/**
|
||||
* Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user
|
||||
*/
|
||||
private readonly _isLocked: UIEventSource<boolean>;
|
||||
private readonly _isLocked: UIEventSource<boolean>
|
||||
|
||||
/**
|
||||
* The callback over the permission API
|
||||
* @private
|
||||
*/
|
||||
private readonly _permission: UIEventSource<string>;
|
||||
private readonly _permission: UIEventSource<string>
|
||||
/**
|
||||
* Literally: _currentGPSLocation.data != undefined
|
||||
* @private
|
||||
*/
|
||||
private readonly _hasLocation: Store<boolean>;
|
||||
private readonly _currentGPSLocation: UIEventSource<Coordinates>;
|
||||
private readonly _hasLocation: Store<boolean>
|
||||
private readonly _currentGPSLocation: UIEventSource<GeolocationCoordinates>
|
||||
/**
|
||||
* Kept in order to update the marker
|
||||
* @private
|
||||
*/
|
||||
private readonly _leafletMap: UIEventSource<L.Map>;
|
||||
private readonly _leafletMap: UIEventSource<L.Map>
|
||||
|
||||
/**
|
||||
* The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs
|
||||
*/
|
||||
private _lastUserRequest: UIEventSource<Date>;
|
||||
private _lastUserRequest: UIEventSource<Date>
|
||||
|
||||
/**
|
||||
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
|
||||
|
@ -64,54 +63,52 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
* If the user denies the geolocation this time, we unset this flag
|
||||
* @private
|
||||
*/
|
||||
private readonly _previousLocationGrant: UIEventSource<string>;
|
||||
private readonly _layoutToUse: LayoutConfig;
|
||||
private readonly _previousLocationGrant: UIEventSource<string>
|
||||
private readonly _layoutToUse: LayoutConfig
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
selectedElement: UIEventSource<any>;
|
||||
currentUserLocation?: SimpleFeatureSource,
|
||||
leafletMap: UIEventSource<any>,
|
||||
layoutToUse: LayoutConfig,
|
||||
featureSwitchGeolocation: UIEventSource<boolean>
|
||||
}
|
||||
) {
|
||||
const currentGPSLocation = new UIEventSource<Coordinates>(undefined, "GPS-coordinate")
|
||||
constructor(state: {
|
||||
selectedElement: UIEventSource<any>
|
||||
currentUserLocation?: SimpleFeatureSource
|
||||
leafletMap: UIEventSource<any>
|
||||
layoutToUse: LayoutConfig
|
||||
featureSwitchGeolocation: UIEventSource<boolean>
|
||||
}) {
|
||||
const currentGPSLocation = new UIEventSource<GeolocationCoordinates>(
|
||||
undefined,
|
||||
"GPS-coordinate"
|
||||
)
|
||||
const leafletMap = state.leafletMap
|
||||
const initedAt = new Date()
|
||||
let autozoomDone = false;
|
||||
const hasLocation = currentGPSLocation.map(
|
||||
(location) => location !== undefined
|
||||
);
|
||||
const previousLocationGrant = LocalStorageSource.Get(
|
||||
"geolocation-permissions"
|
||||
);
|
||||
const isActive = new UIEventSource<boolean>(false);
|
||||
const isLocked = new UIEventSource<boolean>(false);
|
||||
const permission = new UIEventSource<string>("");
|
||||
const lastClick = new UIEventSource<Date>(undefined);
|
||||
const lastClickWithinThreeSecs = lastClick.map(lastClick => {
|
||||
let autozoomDone = false
|
||||
const hasLocation = currentGPSLocation.map((location) => location !== undefined)
|
||||
const previousLocationGrant = LocalStorageSource.Get("geolocation-permissions")
|
||||
const isActive = new UIEventSource<boolean>(false)
|
||||
const isLocked = new UIEventSource<boolean>(false)
|
||||
const permission = new UIEventSource<string>("")
|
||||
const lastClick = new UIEventSource<Date>(undefined)
|
||||
const lastClickWithinThreeSecs = lastClick.map((lastClick) => {
|
||||
if (lastClick === undefined) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000
|
||||
return timeDiff <= 3
|
||||
})
|
||||
|
||||
const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon")
|
||||
const willFocus = lastClick.map(lastUserRequest => {
|
||||
const latLonGiven =
|
||||
QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon")
|
||||
const willFocus = lastClick.map((lastUserRequest) => {
|
||||
const timeDiffInited = (new Date().getTime() - initedAt.getTime()) / 1000
|
||||
if (!latLonGiven && !autozoomDone && timeDiffInited < Constants.zoomToLocationTimeout) {
|
||||
return true
|
||||
}
|
||||
if (lastUserRequest === undefined) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
const timeDiff = (new Date().getTime() - lastUserRequest.getTime()) / 1000
|
||||
return timeDiff <= Constants.zoomToLocationTimeout
|
||||
})
|
||||
|
||||
lastClick.addCallbackAndRunD(_ => {
|
||||
lastClick.addCallbackAndRunD((_) => {
|
||||
window.setTimeout(() => {
|
||||
if (lastClickWithinThreeSecs.data || willFocus.data) {
|
||||
lastClick.ping()
|
||||
|
@ -123,7 +120,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
hasLocation.map(
|
||||
(hasLocationData) => {
|
||||
if (permission.data === "denied") {
|
||||
return Svg.location_refused_svg();
|
||||
return Svg.location_refused_svg()
|
||||
}
|
||||
|
||||
if (!isActive.data) {
|
||||
|
@ -134,7 +131,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
// If will focus is active too, we indicate this differently
|
||||
const icon = willFocus.data ? Svg.location_svg() : Svg.location_empty_svg()
|
||||
icon.SetStyle("animation: spin 4s linear infinite;")
|
||||
return icon;
|
||||
return icon
|
||||
}
|
||||
if (isLocked.data) {
|
||||
return Svg.location_locked_svg()
|
||||
|
@ -144,42 +141,41 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
}
|
||||
|
||||
// We have a location, so we show a dot in the center
|
||||
return Svg.location_svg();
|
||||
return Svg.location_svg()
|
||||
},
|
||||
[isActive, isLocked, permission, lastClickWithinThreeSecs, willFocus]
|
||||
)
|
||||
);
|
||||
)
|
||||
this.SetClass("mapcontrol")
|
||||
this._isActive = isActive;
|
||||
this._isLocked = isLocked;
|
||||
this._isActive = isActive
|
||||
this._isLocked = isLocked
|
||||
this._permission = permission
|
||||
this._previousLocationGrant = previousLocationGrant;
|
||||
this._currentGPSLocation = currentGPSLocation;
|
||||
this._leafletMap = leafletMap;
|
||||
this._layoutToUse = state.layoutToUse;
|
||||
this._hasLocation = hasLocation;
|
||||
this._previousLocationGrant = previousLocationGrant
|
||||
this._currentGPSLocation = currentGPSLocation
|
||||
this._leafletMap = leafletMap
|
||||
this._layoutToUse = state.layoutToUse
|
||||
this._hasLocation = hasLocation
|
||||
this._lastUserRequest = lastClick
|
||||
const self = this;
|
||||
const self = this
|
||||
|
||||
const currentPointer = this._isActive.map(
|
||||
(isActive) => {
|
||||
if (isActive && !self._hasLocation.data) {
|
||||
return "cursor-wait";
|
||||
return "cursor-wait"
|
||||
}
|
||||
return "cursor-pointer";
|
||||
return "cursor-pointer"
|
||||
},
|
||||
[this._hasLocation]
|
||||
);
|
||||
)
|
||||
currentPointer.addCallbackAndRun((pointerClass) => {
|
||||
self.RemoveClass("cursor-wait")
|
||||
self.RemoveClass("cursor-pointer")
|
||||
self.SetClass(pointerClass);
|
||||
});
|
||||
|
||||
self.SetClass(pointerClass)
|
||||
})
|
||||
|
||||
this.onClick(() => {
|
||||
/*
|
||||
* If the previous click was within 3 seconds (and we have an active location), then we lock to the location
|
||||
* If the previous click was within 3 seconds (and we have an active location), then we lock to the location
|
||||
*/
|
||||
if (self._hasLocation.data) {
|
||||
if (isLocked.data) {
|
||||
|
@ -197,14 +193,16 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
}
|
||||
}
|
||||
|
||||
self.init(true, true);
|
||||
});
|
||||
self.init(true, true)
|
||||
})
|
||||
|
||||
const doAutoZoomToLocation =
|
||||
!latLonGiven &&
|
||||
state.featureSwitchGeolocation.data &&
|
||||
state.selectedElement.data !== undefined
|
||||
this.init(false, doAutoZoomToLocation)
|
||||
|
||||
const doAutoZoomToLocation = !latLonGiven && state.featureSwitchGeolocation.data && state.selectedElement.data !== undefined
|
||||
this.init(false, doAutoZoomToLocation);
|
||||
|
||||
isLocked.addCallbackAndRunD(isLocked => {
|
||||
isLocked.addCallbackAndRunD((isLocked) => {
|
||||
if (isLocked) {
|
||||
leafletMap.data?.dragging?.disable()
|
||||
} else {
|
||||
|
@ -214,47 +212,45 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
|
||||
this.currentLocation = state.currentUserLocation
|
||||
this._currentGPSLocation.addCallback((location) => {
|
||||
self._previousLocationGrant.setData("granted");
|
||||
self._previousLocationGrant.setData("granted")
|
||||
const feature = {
|
||||
"type": "Feature",
|
||||
type: "Feature",
|
||||
properties: <GeoLocationPointProperties>{
|
||||
id: "gps",
|
||||
"user:location": "yes",
|
||||
"date": new Date().toISOString(),
|
||||
"latitude": location.latitude,
|
||||
"longitude": location.longitude,
|
||||
"speed": location.speed,
|
||||
"accuracy": location.accuracy,
|
||||
"heading": location.heading,
|
||||
"altitude": location.altitude
|
||||
date: new Date().toISOString(),
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
speed: location.speed,
|
||||
accuracy: location.accuracy,
|
||||
heading: location.heading,
|
||||
altitude: location.altitude,
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [location.longitude, location.latitude],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
self.currentLocation?.features?.setData([{feature, freshness: new Date()}])
|
||||
self.currentLocation?.features?.setData([{ feature, freshness: new Date() }])
|
||||
|
||||
if (willFocus.data) {
|
||||
console.log("Zooming to user location: willFocus is set")
|
||||
lastClick.setData(undefined);
|
||||
autozoomDone = true;
|
||||
self.MoveToCurrentLocation(16);
|
||||
lastClick.setData(undefined)
|
||||
autozoomDone = true
|
||||
self.MoveToCurrentLocation(16)
|
||||
} else if (self._isLocked.data) {
|
||||
self.MoveToCurrentLocation();
|
||||
self.MoveToCurrentLocation()
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
private init(askPermission: boolean, zoomToLocation: boolean) {
|
||||
const self = this;
|
||||
const self = this
|
||||
|
||||
if (self._isActive.data) {
|
||||
self.MoveToCurrentLocation(16);
|
||||
return;
|
||||
self.MoveToCurrentLocation(16)
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof navigator === "undefined") {
|
||||
|
@ -262,27 +258,25 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
}
|
||||
|
||||
try {
|
||||
navigator?.permissions
|
||||
?.query({name: "geolocation"})
|
||||
?.then(function (status) {
|
||||
console.log("Geolocation permission is ", status.state);
|
||||
if (status.state === "granted") {
|
||||
self.StartGeolocating(zoomToLocation);
|
||||
}
|
||||
self._permission.setData(status.state);
|
||||
status.onchange = function () {
|
||||
self._permission.setData(status.state);
|
||||
};
|
||||
});
|
||||
navigator?.permissions?.query({ name: "geolocation" })?.then(function (status) {
|
||||
console.log("Geolocation permission is ", status.state)
|
||||
if (status.state === "granted") {
|
||||
self.StartGeolocating(zoomToLocation)
|
||||
}
|
||||
self._permission.setData(status.state)
|
||||
status.onchange = function () {
|
||||
self._permission.setData(status.state)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
if (askPermission) {
|
||||
self.StartGeolocating(zoomToLocation);
|
||||
self.StartGeolocating(zoomToLocation)
|
||||
} else if (this._previousLocationGrant.data === "granted") {
|
||||
this._previousLocationGrant.setData("");
|
||||
self.StartGeolocating(zoomToLocation);
|
||||
this._previousLocationGrant.setData("")
|
||||
self.StartGeolocating(zoomToLocation)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -311,7 +305,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
* handler._currentGPSLocation.setData(<any> {latitude : 60, longitude: 60) // out of bounds
|
||||
* handler.MoveToCurrentLocation()
|
||||
* resultingLocation // => [60, 60]
|
||||
*
|
||||
*
|
||||
* // should refuse to move if out of bounds
|
||||
* let resultingLocation = undefined
|
||||
* let resultingzoom = 1
|
||||
|
@ -322,7 +316,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
* layoutToUse: new LayoutConfig(<any>{
|
||||
* id: 'test',
|
||||
* title: {"en":"test"}
|
||||
* "lockLocation": [ [ 2.1, 50.4], [6.4, 51.54 ]],
|
||||
* "lockLocation": [ [ 2.1, 50.4], [6.4, 51.54 ]],
|
||||
* description: "A testing theme",
|
||||
* layers: []
|
||||
* }),
|
||||
|
@ -337,20 +331,20 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
* resultingLocation // => [51.3, 4.1]
|
||||
*/
|
||||
private MoveToCurrentLocation(targetZoom?: number) {
|
||||
const location = this._currentGPSLocation.data;
|
||||
this._lastUserRequest.setData(undefined);
|
||||
const location = this._currentGPSLocation.data
|
||||
this._lastUserRequest.setData(undefined)
|
||||
|
||||
if (
|
||||
this._currentGPSLocation.data.latitude === 0 &&
|
||||
this._currentGPSLocation.data.longitude === 0
|
||||
) {
|
||||
console.debug("Not moving to GPS-location: it is null island");
|
||||
return;
|
||||
console.debug("Not moving to GPS-location: it is null island")
|
||||
return
|
||||
}
|
||||
|
||||
// We check that the GPS location is not out of bounds
|
||||
const b = this._layoutToUse.lockLocation;
|
||||
let inRange = true;
|
||||
const b = this._layoutToUse.lockLocation
|
||||
let inRange = true
|
||||
if (b) {
|
||||
if (b !== true) {
|
||||
// B is an array with our locklocation
|
||||
|
@ -358,41 +352,44 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
}
|
||||
}
|
||||
if (!inRange) {
|
||||
console.log("Not zooming to GPS location: out of bounds", b, location);
|
||||
console.log("Not zooming to GPS location: out of bounds", b, location)
|
||||
} else {
|
||||
const currentZoom = this._leafletMap.data.getZoom()
|
||||
this._leafletMap.data.setView([location.latitude, location.longitude], Math.max(targetZoom ?? 0, currentZoom));
|
||||
this._leafletMap.data.setView(
|
||||
[location.latitude, location.longitude],
|
||||
Math.max(targetZoom ?? 0, currentZoom)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private StartGeolocating(zoomToGPS = true) {
|
||||
const self = this;
|
||||
const self = this
|
||||
|
||||
this._lastUserRequest.setData(zoomToGPS ? new Date() : new Date(0))
|
||||
if (self._permission.data === "denied") {
|
||||
self._previousLocationGrant.setData("");
|
||||
self._previousLocationGrant.setData("")
|
||||
self._isActive.setData(false)
|
||||
return "";
|
||||
return ""
|
||||
}
|
||||
if (this._currentGPSLocation.data !== undefined) {
|
||||
this.MoveToCurrentLocation(16);
|
||||
this.MoveToCurrentLocation(16)
|
||||
}
|
||||
|
||||
if (self._isActive.data) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
self._isActive.setData(true);
|
||||
self._isActive.setData(true)
|
||||
|
||||
navigator.geolocation.watchPosition(
|
||||
function (position) {
|
||||
self._currentGPSLocation.setData(position.coords);
|
||||
self._currentGPSLocation.setData(position.coords)
|
||||
},
|
||||
function () {
|
||||
console.warn("Could not get location with navigator.geolocation");
|
||||
console.warn("Could not get location with navigator.geolocation")
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true
|
||||
enableHighAccuracy: true,
|
||||
}
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,112 +1,124 @@
|
|||
import {Store, UIEventSource} from "../UIEventSource";
|
||||
import {Or} from "../Tags/Or";
|
||||
import {Overpass} from "../Osm/Overpass";
|
||||
import FeatureSource from "../FeatureSource/FeatureSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import {TagsFilter} from "../Tags/TagsFilter";
|
||||
import SimpleMetaTagger from "../SimpleMetaTagger";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import RelationsTracker from "../Osm/RelationsTracker";
|
||||
import {BBox} from "../BBox";
|
||||
import Loc from "../../Models/Loc";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import Constants from "../../Models/Constants";
|
||||
import TileFreshnessCalculator from "../FeatureSource/TileFreshnessCalculator";
|
||||
import {Tiles} from "../../Models/TileRange";
|
||||
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { Or } from "../Tags/Or"
|
||||
import { Overpass } from "../Osm/Overpass"
|
||||
import FeatureSource from "../FeatureSource/FeatureSource"
|
||||
import { Utils } from "../../Utils"
|
||||
import { TagsFilter } from "../Tags/TagsFilter"
|
||||
import SimpleMetaTagger from "../SimpleMetaTagger"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import RelationsTracker from "../Osm/RelationsTracker"
|
||||
import { BBox } from "../BBox"
|
||||
import Loc from "../../Models/Loc"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Constants from "../../Models/Constants"
|
||||
import TileFreshnessCalculator from "../FeatureSource/TileFreshnessCalculator"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
|
||||
export default class OverpassFeatureSource implements FeatureSource {
|
||||
|
||||
public readonly name = "OverpassFeatureSource"
|
||||
|
||||
/**
|
||||
* The last loaded features of the geojson
|
||||
*/
|
||||
public readonly features: UIEventSource<{ feature: any, freshness: Date }[]> = new UIEventSource<any[]>(undefined);
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> =
|
||||
new UIEventSource<any[]>(undefined)
|
||||
|
||||
public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0)
|
||||
|
||||
public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0);
|
||||
public readonly relationsTracker: RelationsTracker
|
||||
|
||||
public readonly relationsTracker: RelationsTracker;
|
||||
|
||||
|
||||
private readonly retries: UIEventSource<number> = new UIEventSource<number>(0);
|
||||
private readonly retries: UIEventSource<number> = new UIEventSource<number>(0)
|
||||
|
||||
private readonly state: {
|
||||
readonly locationControl: Store<Loc>,
|
||||
readonly layoutToUse: LayoutConfig,
|
||||
readonly overpassUrl: Store<string[]>;
|
||||
readonly overpassTimeout: Store<number>;
|
||||
readonly locationControl: Store<Loc>
|
||||
readonly layoutToUse: LayoutConfig
|
||||
readonly overpassUrl: Store<string[]>
|
||||
readonly overpassTimeout: Store<number>
|
||||
readonly currentBounds: Store<BBox>
|
||||
}
|
||||
private readonly _isActive: Store<boolean>
|
||||
/**
|
||||
* Callback to handle all the data
|
||||
*/
|
||||
private readonly onBboxLoaded: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void;
|
||||
private readonly onBboxLoaded: (
|
||||
bbox: BBox,
|
||||
date: Date,
|
||||
layers: LayerConfig[],
|
||||
zoomlevel: number
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Keeps track of how fresh the data is
|
||||
* @private
|
||||
*/
|
||||
private readonly freshnesses: Map<string, TileFreshnessCalculator>;
|
||||
private readonly freshnesses: Map<string, TileFreshnessCalculator>
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
readonly locationControl: Store<Loc>,
|
||||
readonly layoutToUse: LayoutConfig,
|
||||
readonly overpassUrl: Store<string[]>;
|
||||
readonly overpassTimeout: Store<number>;
|
||||
readonly overpassMaxZoom: Store<number>,
|
||||
readonly locationControl: Store<Loc>
|
||||
readonly layoutToUse: LayoutConfig
|
||||
readonly overpassUrl: Store<string[]>
|
||||
readonly overpassTimeout: Store<number>
|
||||
readonly overpassMaxZoom: Store<number>
|
||||
readonly currentBounds: Store<BBox>
|
||||
},
|
||||
options: {
|
||||
padToTiles: Store<number>,
|
||||
isActive?: Store<boolean>,
|
||||
relationTracker: RelationsTracker,
|
||||
onBboxLoaded?: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void,
|
||||
padToTiles: Store<number>
|
||||
isActive?: Store<boolean>
|
||||
relationTracker: RelationsTracker
|
||||
onBboxLoaded?: (
|
||||
bbox: BBox,
|
||||
date: Date,
|
||||
layers: LayerConfig[],
|
||||
zoomlevel: number
|
||||
) => void
|
||||
freshnesses?: Map<string, TileFreshnessCalculator>
|
||||
}) {
|
||||
|
||||
}
|
||||
) {
|
||||
this.state = state
|
||||
this._isActive = options.isActive;
|
||||
this._isActive = options.isActive
|
||||
this.onBboxLoaded = options.onBboxLoaded
|
||||
this.relationsTracker = options.relationTracker
|
||||
this.freshnesses = options.freshnesses
|
||||
const self = this;
|
||||
state.currentBounds.addCallback(_ => {
|
||||
const self = this
|
||||
state.currentBounds.addCallback((_) => {
|
||||
self.update(options.padToTiles.data)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private GetFilter(interpreterUrl: string, layersToDownload: LayerConfig[]): Overpass {
|
||||
let filters: TagsFilter[] = [];
|
||||
let extraScripts: string[] = [];
|
||||
let filters: TagsFilter[] = []
|
||||
let extraScripts: string[] = []
|
||||
for (const layer of layersToDownload) {
|
||||
if (layer.source.overpassScript !== undefined) {
|
||||
extraScripts.push(layer.source.overpassScript)
|
||||
} else {
|
||||
filters.push(layer.source.osmTags);
|
||||
filters.push(layer.source.osmTags)
|
||||
}
|
||||
}
|
||||
filters = Utils.NoNull(filters)
|
||||
extraScripts = Utils.NoNull(extraScripts)
|
||||
if (filters.length + extraScripts.length === 0) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return new Overpass(new Or(filters), extraScripts, interpreterUrl, this.state.overpassTimeout, this.relationsTracker);
|
||||
return new Overpass(
|
||||
new Or(filters),
|
||||
extraScripts,
|
||||
interpreterUrl,
|
||||
this.state.overpassTimeout,
|
||||
this.relationsTracker
|
||||
)
|
||||
}
|
||||
|
||||
private update(paddedZoomLevel: number) {
|
||||
if (!this._isActive.data) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const self = this;
|
||||
this.updateAsync(paddedZoomLevel).then(bboxDate => {
|
||||
const self = this
|
||||
this.updateAsync(paddedZoomLevel).then((bboxDate) => {
|
||||
if (bboxDate === undefined || self.onBboxLoaded === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const [bbox, date, layers] = bboxDate
|
||||
self.onBboxLoaded(bbox, date, layers, paddedZoomLevel)
|
||||
|
@ -115,56 +127,58 @@ export default class OverpassFeatureSource implements FeatureSource {
|
|||
|
||||
private async updateAsync(padToZoomLevel: number): Promise<[BBox, Date, LayerConfig[]]> {
|
||||
if (this.runningQuery.data) {
|
||||
console.log("Still running a query, not updating");
|
||||
return undefined;
|
||||
console.log("Still running a query, not updating")
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (this.timeout.data > 0) {
|
||||
console.log("Still in timeout - not updating")
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
let data: any = undefined
|
||||
let date: Date = undefined
|
||||
let lastUsed = 0;
|
||||
|
||||
let lastUsed = 0
|
||||
|
||||
const layersToDownload = []
|
||||
const neededTiles = this.state.currentBounds.data.expandToTileBounds(padToZoomLevel).containingTileRange(padToZoomLevel)
|
||||
const neededTiles = this.state.currentBounds.data
|
||||
.expandToTileBounds(padToZoomLevel)
|
||||
.containingTileRange(padToZoomLevel)
|
||||
for (const layer of this.state.layoutToUse.layers) {
|
||||
|
||||
if (typeof (layer) === "string") {
|
||||
if (typeof layer === "string") {
|
||||
throw "A layer was not expanded!"
|
||||
}
|
||||
if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
|
||||
continue
|
||||
}
|
||||
if (this.state.locationControl.data.zoom < layer.minzoom) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (layer.doNotDownload) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (layer.source.geojsonSource !== undefined) {
|
||||
// Not our responsibility to download this layer!
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
const freshness = this.freshnesses?.get(layer.id)
|
||||
if (freshness !== undefined) {
|
||||
const oldestDataDate = Math.min(...Tiles.MapRange(neededTiles, (x, y) => {
|
||||
const date = freshness.freshnessFor(padToZoomLevel, x, y);
|
||||
if (date === undefined) {
|
||||
return 0
|
||||
}
|
||||
return date.getTime()
|
||||
})) / 1000;
|
||||
const oldestDataDate =
|
||||
Math.min(
|
||||
...Tiles.MapRange(neededTiles, (x, y) => {
|
||||
const date = freshness.freshnessFor(padToZoomLevel, x, y)
|
||||
if (date === undefined) {
|
||||
return 0
|
||||
}
|
||||
return date.getTime()
|
||||
})
|
||||
) / 1000
|
||||
const now = new Date().getTime()
|
||||
const minRequiredAge = (now / 1000) - layer.maxAgeOfCache
|
||||
const minRequiredAge = now / 1000 - layer.maxAgeOfCache
|
||||
if (oldestDataDate >= minRequiredAge) {
|
||||
// still fresh enough - not updating
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
layersToDownload.push(layer)
|
||||
|
@ -172,34 +186,35 @@ export default class OverpassFeatureSource implements FeatureSource {
|
|||
|
||||
if (layersToDownload.length == 0) {
|
||||
console.debug("Not updating - no layers needed")
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const self = this
|
||||
const overpassUrls = self.state.overpassUrl.data
|
||||
let bounds: BBox
|
||||
do {
|
||||
try {
|
||||
|
||||
bounds = this.state.currentBounds.data?.pad(this.state.layoutToUse.widenFactor)?.expandToTileBounds(padToZoomLevel);
|
||||
bounds = this.state.currentBounds.data
|
||||
?.pad(this.state.layoutToUse.widenFactor)
|
||||
?.expandToTileBounds(padToZoomLevel)
|
||||
|
||||
if (bounds === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload);
|
||||
const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload)
|
||||
|
||||
if (overpass === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
this.runningQuery.setData(true);
|
||||
this.runningQuery.setData(true)
|
||||
|
||||
[data, date] = await overpass.queryGeoJson(bounds)
|
||||
;[data, date] = await overpass.queryGeoJson(bounds)
|
||||
console.log("Querying overpass is done", data)
|
||||
} catch (e) {
|
||||
self.retries.data++;
|
||||
self.retries.ping();
|
||||
console.error(`QUERY FAILED due to`, e);
|
||||
self.retries.data++
|
||||
self.retries.ping()
|
||||
console.error(`QUERY FAILED due to`, e)
|
||||
|
||||
await Utils.waitFor(1000)
|
||||
|
||||
|
@ -208,34 +223,38 @@ export default class OverpassFeatureSource implements FeatureSource {
|
|||
console.log("Trying next time with", overpassUrls[lastUsed])
|
||||
} else {
|
||||
lastUsed = 0
|
||||
self.timeout.setData(self.retries.data * 5);
|
||||
self.timeout.setData(self.retries.data * 5)
|
||||
|
||||
while (self.timeout.data > 0) {
|
||||
await Utils.waitFor(1000)
|
||||
console.log(self.timeout.data)
|
||||
self.timeout.data--
|
||||
self.timeout.ping();
|
||||
self.timeout.ping()
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (data === undefined && this._isActive.data);
|
||||
|
||||
} while (data === undefined && this._isActive.data)
|
||||
|
||||
try {
|
||||
if (data === undefined) {
|
||||
return undefined
|
||||
}
|
||||
data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date, undefined, this.state));
|
||||
self.features.setData(data.features.map(f => ({feature: f, freshness: date})));
|
||||
return [bounds, date, layersToDownload];
|
||||
data.features.forEach((feature) =>
|
||||
SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(
|
||||
feature,
|
||||
date,
|
||||
undefined,
|
||||
this.state
|
||||
)
|
||||
)
|
||||
self.features.setData(data.features.map((f) => ({ feature: f, freshness: date })))
|
||||
return [bounds, date, layersToDownload]
|
||||
} catch (e) {
|
||||
console.error("Got the overpass response, but could not process it: ", e, e.stack)
|
||||
return undefined
|
||||
} finally {
|
||||
self.retries.setData(0);
|
||||
self.runningQuery.setData(false);
|
||||
self.retries.setData(0)
|
||||
self.runningQuery.setData(false)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +1,42 @@
|
|||
import {Changes} from "../Osm/Changes";
|
||||
import Constants from "../../Models/Constants";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import { Changes } from "../Osm/Changes"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export default class PendingChangesUploader {
|
||||
|
||||
private lastChange: Date;
|
||||
private lastChange: Date
|
||||
|
||||
constructor(changes: Changes, selectedFeature: UIEventSource<any>) {
|
||||
const self = this;
|
||||
this.lastChange = new Date();
|
||||
const self = this
|
||||
this.lastChange = new Date()
|
||||
changes.pendingChanges.addCallback(() => {
|
||||
self.lastChange = new Date();
|
||||
self.lastChange = new Date()
|
||||
|
||||
window.setTimeout(() => {
|
||||
const diff = (new Date().getTime() - self.lastChange.getTime()) / 1000;
|
||||
const diff = (new Date().getTime() - self.lastChange.getTime()) / 1000
|
||||
if (Constants.updateTimeoutSec >= diff - 1) {
|
||||
changes.flushChanges("Flushing changes due to timeout");
|
||||
changes.flushChanges("Flushing changes due to timeout")
|
||||
}
|
||||
}, Constants.updateTimeoutSec * 1000);
|
||||
});
|
||||
}, Constants.updateTimeoutSec * 1000)
|
||||
})
|
||||
|
||||
|
||||
selectedFeature
|
||||
.stabilized(10000)
|
||||
.addCallback(feature => {
|
||||
if (feature === undefined) {
|
||||
// The popup got closed - we flush
|
||||
changes.flushChanges("Flushing changes due to popup closed");
|
||||
}
|
||||
});
|
||||
selectedFeature.stabilized(10000).addCallback((feature) => {
|
||||
if (feature === undefined) {
|
||||
// The popup got closed - we flush
|
||||
changes.flushChanges("Flushing changes due to popup closed")
|
||||
}
|
||||
})
|
||||
|
||||
if (Utils.runningFromConsole) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
document.addEventListener('mouseout', e => {
|
||||
document.addEventListener("mouseout", (e) => {
|
||||
// @ts-ignore
|
||||
if (!e.toElement && !e.relatedTarget) {
|
||||
changes.flushChanges("Flushing changes due to focus lost");
|
||||
changes.flushChanges("Flushing changes due to focus lost")
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
document.onfocus = () => {
|
||||
changes.flushChanges("OnFocus")
|
||||
|
@ -50,28 +46,28 @@ export default class PendingChangesUploader {
|
|||
changes.flushChanges("OnFocus")
|
||||
}
|
||||
try {
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
changes.flushChanges("Visibility change")
|
||||
}, false);
|
||||
document.addEventListener(
|
||||
"visibilitychange",
|
||||
() => {
|
||||
changes.flushChanges("Visibility change")
|
||||
},
|
||||
false
|
||||
)
|
||||
} catch (e) {
|
||||
console.warn("Could not register visibility change listener", e)
|
||||
}
|
||||
|
||||
|
||||
function onunload(e) {
|
||||
if (changes.pendingChanges.data.length == 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
changes.flushChanges("onbeforeunload - probably closing or something similar");
|
||||
e.preventDefault();
|
||||
changes.flushChanges("onbeforeunload - probably closing or something similar")
|
||||
e.preventDefault()
|
||||
return "Saving your last changes..."
|
||||
}
|
||||
|
||||
window.onbeforeunload = onunload
|
||||
// https://stackoverflow.com/questions/3239834/window-onbeforeunload-not-working-on-the-ipad#4824156
|
||||
window.addEventListener("pagehide", onunload)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,51 +1,47 @@
|
|||
/**
|
||||
* This actor will download the latest version of the selected element from OSM and update the tags if necessary.
|
||||
*/
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import {Changes} from "../Osm/Changes";
|
||||
import {OsmObject} from "../Osm/OsmObject";
|
||||
import {OsmConnection} from "../Osm/OsmConnection";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import SimpleMetaTagger from "../SimpleMetaTagger";
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import { ElementStorage } from "../ElementStorage"
|
||||
import { Changes } from "../Osm/Changes"
|
||||
import { OsmObject } from "../Osm/OsmObject"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import SimpleMetaTagger from "../SimpleMetaTagger"
|
||||
|
||||
export default class SelectedElementTagsUpdater {
|
||||
|
||||
private static readonly metatags = new Set(["timestamp",
|
||||
private static readonly metatags = new Set([
|
||||
"timestamp",
|
||||
"version",
|
||||
"changeset",
|
||||
"user",
|
||||
"uid",
|
||||
"id"])
|
||||
"id",
|
||||
])
|
||||
|
||||
constructor(state: {
|
||||
selectedElement: UIEventSource<any>,
|
||||
allElements: ElementStorage,
|
||||
changes: Changes,
|
||||
osmConnection: OsmConnection,
|
||||
selectedElement: UIEventSource<any>
|
||||
allElements: ElementStorage
|
||||
changes: Changes
|
||||
osmConnection: OsmConnection
|
||||
layoutToUse: LayoutConfig
|
||||
}) {
|
||||
|
||||
|
||||
state.osmConnection.isLoggedIn.addCallbackAndRun(isLoggedIn => {
|
||||
state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => {
|
||||
if (isLoggedIn) {
|
||||
SelectedElementTagsUpdater.installCallback(state)
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public static installCallback(state: {
|
||||
selectedElement: UIEventSource<any>,
|
||||
allElements: ElementStorage,
|
||||
changes: Changes,
|
||||
osmConnection: OsmConnection,
|
||||
selectedElement: UIEventSource<any>
|
||||
allElements: ElementStorage
|
||||
changes: Changes
|
||||
osmConnection: OsmConnection
|
||||
layoutToUse: LayoutConfig
|
||||
}) {
|
||||
|
||||
|
||||
state.selectedElement.addCallbackAndRunD(s => {
|
||||
state.selectedElement.addCallbackAndRunD((s) => {
|
||||
let id = s.properties?.id
|
||||
|
||||
const backendUrl = state.osmConnection._oauth_config.url
|
||||
|
@ -55,31 +51,31 @@ export default class SelectedElementTagsUpdater {
|
|||
|
||||
if (!(id.startsWith("way") || id.startsWith("node") || id.startsWith("relation"))) {
|
||||
// This object is _not_ from OSM, so we skip it!
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (id.indexOf("-") >= 0) {
|
||||
// This is a new object
|
||||
return;
|
||||
return
|
||||
}
|
||||
OsmObject.DownloadPropertiesOf(id).then(latestTags => {
|
||||
OsmObject.DownloadPropertiesOf(id).then((latestTags) => {
|
||||
SelectedElementTagsUpdater.applyUpdate(state, latestTags, id)
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
public static applyUpdate(state: {
|
||||
selectedElement: UIEventSource<any>,
|
||||
allElements: ElementStorage,
|
||||
changes: Changes,
|
||||
osmConnection: OsmConnection,
|
||||
layoutToUse: LayoutConfig
|
||||
}, latestTags: any, id: string
|
||||
public static applyUpdate(
|
||||
state: {
|
||||
selectedElement: UIEventSource<any>
|
||||
allElements: ElementStorage
|
||||
changes: Changes
|
||||
osmConnection: OsmConnection
|
||||
layoutToUse: LayoutConfig
|
||||
},
|
||||
latestTags: any,
|
||||
id: string
|
||||
) {
|
||||
try {
|
||||
|
||||
const leftRightSensitive = state.layoutToUse.isLeftRightSensitive()
|
||||
|
||||
if (leftRightSensitive) {
|
||||
|
@ -87,11 +83,11 @@ export default class SelectedElementTagsUpdater {
|
|||
}
|
||||
|
||||
const pendingChanges = state.changes.pendingChanges.data
|
||||
.filter(change => change.type + "/" + change.id === id)
|
||||
.filter(change => change.tags !== undefined);
|
||||
.filter((change) => change.type + "/" + change.id === id)
|
||||
.filter((change) => change.tags !== undefined)
|
||||
|
||||
for (const pendingChange of pendingChanges) {
|
||||
const tagChanges = pendingChange.tags;
|
||||
const tagChanges = pendingChange.tags
|
||||
for (const tagChange of tagChanges) {
|
||||
const key = tagChange.k
|
||||
const v = tagChange.v
|
||||
|
@ -103,10 +99,9 @@ export default class SelectedElementTagsUpdater {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// With the changes applied, we merge them onto the upstream object
|
||||
let somethingChanged = false;
|
||||
const currentTagsSource = state.allElements.getEventSourceById(id);
|
||||
let somethingChanged = false
|
||||
const currentTagsSource = state.allElements.getEventSourceById(id)
|
||||
const currentTags = currentTagsSource.data
|
||||
for (const key in latestTags) {
|
||||
let osmValue = latestTags[key]
|
||||
|
@ -117,7 +112,7 @@ export default class SelectedElementTagsUpdater {
|
|||
|
||||
const localValue = currentTags[key]
|
||||
if (localValue !== osmValue) {
|
||||
somethingChanged = true;
|
||||
somethingChanged = true
|
||||
currentTags[key] = osmValue
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +132,6 @@ export default class SelectedElementTagsUpdater {
|
|||
somethingChanged = true
|
||||
}
|
||||
|
||||
|
||||
if (somethingChanged) {
|
||||
console.log("Detected upstream changes to the object when opening it, updating...")
|
||||
currentTagsSource.ping()
|
||||
|
@ -148,6 +142,4 @@ export default class SelectedElementTagsUpdater {
|
|||
console.error("Updating the tags of selected element ", id, "failed due to", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,63 +1,67 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {OsmObject} from "../Osm/OsmObject";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import FeaturePipeline from "../FeatureSource/FeaturePipeline";
|
||||
import {GeoOperations} from "../GeoOperations";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import { OsmObject } from "../Osm/OsmObject"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { ElementStorage } from "../ElementStorage"
|
||||
import FeaturePipeline from "../FeatureSource/FeaturePipeline"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
|
||||
/**
|
||||
* Makes sure the hash shows the selected element and vice-versa.
|
||||
*/
|
||||
export default class SelectedFeatureHandler {
|
||||
private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "filters", "location_track", "", undefined])
|
||||
private readonly hash: UIEventSource<string>;
|
||||
private static readonly _no_trigger_on = new Set([
|
||||
"welcome",
|
||||
"copyright",
|
||||
"layers",
|
||||
"new",
|
||||
"filters",
|
||||
"location_track",
|
||||
"",
|
||||
undefined,
|
||||
])
|
||||
private readonly hash: UIEventSource<string>
|
||||
private readonly state: {
|
||||
selectedElement: UIEventSource<any>,
|
||||
allElements: ElementStorage,
|
||||
locationControl: UIEventSource<Loc>,
|
||||
selectedElement: UIEventSource<any>
|
||||
allElements: ElementStorage
|
||||
locationControl: UIEventSource<Loc>
|
||||
layoutToUse: LayoutConfig
|
||||
}
|
||||
|
||||
constructor(
|
||||
hash: UIEventSource<string>,
|
||||
state: {
|
||||
selectedElement: UIEventSource<any>,
|
||||
allElements: ElementStorage,
|
||||
featurePipeline: FeaturePipeline,
|
||||
locationControl: UIEventSource<Loc>,
|
||||
selectedElement: UIEventSource<any>
|
||||
allElements: ElementStorage
|
||||
featurePipeline: FeaturePipeline
|
||||
locationControl: UIEventSource<Loc>
|
||||
layoutToUse: LayoutConfig
|
||||
}
|
||||
) {
|
||||
this.hash = hash;
|
||||
this.hash = hash
|
||||
this.state = state
|
||||
|
||||
|
||||
// If the hash changes, set the selected element correctly
|
||||
|
||||
const self = this;
|
||||
const self = this
|
||||
hash.addCallback(() => self.setSelectedElementFromHash())
|
||||
|
||||
|
||||
state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD(_ => {
|
||||
state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD((_) => {
|
||||
// New data was loaded. In initial startup, the hash might be set (via the URL) but might not be selected yet
|
||||
if (hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)) {
|
||||
// This is an invalid hash anyway
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (state.selectedElement.data !== undefined) {
|
||||
// We already have something selected
|
||||
return;
|
||||
return
|
||||
}
|
||||
self.setSelectedElementFromHash()
|
||||
})
|
||||
|
||||
|
||||
this.initialLoad()
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* On startup: check if the hash is loaded and eventually zoom to it
|
||||
* @private
|
||||
|
@ -65,21 +69,18 @@ export default class SelectedFeatureHandler {
|
|||
private initialLoad() {
|
||||
const hash = this.hash.data
|
||||
if (hash === undefined || hash === "" || hash.indexOf("-") >= 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (SelectedFeatureHandler._no_trigger_on.has(hash)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!(hash.startsWith("node") || hash.startsWith("way") || hash.startsWith("relation"))) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
OsmObject.DownloadObjectAsync(hash).then(obj => {
|
||||
|
||||
OsmObject.DownloadObjectAsync(hash).then((obj) => {
|
||||
try {
|
||||
|
||||
console.log("Downloaded selected object from OSM-API for initial load: ", hash)
|
||||
const geojson = obj.asGeoJson()
|
||||
this.state.allElements.addOrGetElement(geojson)
|
||||
|
@ -88,9 +89,7 @@ export default class SelectedFeatureHandler {
|
|||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private setSelectedElementFromHash() {
|
||||
|
@ -98,22 +97,21 @@ export default class SelectedFeatureHandler {
|
|||
const h = this.hash.data
|
||||
if (h === undefined || h === "") {
|
||||
// Hash has been cleared - we clear the selected element
|
||||
state.selectedElement.setData(undefined);
|
||||
state.selectedElement.setData(undefined)
|
||||
} else {
|
||||
|
||||
// we search the element to select
|
||||
const feature = state.allElements.ContainingFeatures.get(h)
|
||||
if (feature === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const currentlySeleced = state.selectedElement.data
|
||||
if (currentlySeleced === undefined) {
|
||||
state.selectedElement.setData(feature)
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (currentlySeleced.properties?.id === feature.properties.id) {
|
||||
// We already have the right feature
|
||||
return;
|
||||
return
|
||||
}
|
||||
state.selectedElement.setData(feature)
|
||||
}
|
||||
|
@ -121,25 +119,24 @@ export default class SelectedFeatureHandler {
|
|||
|
||||
// If a feature is selected via the hash, zoom there
|
||||
private zoomToSelectedFeature() {
|
||||
|
||||
const selected = this.state.selectedElement.data
|
||||
if (selected === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const centerpoint = GeoOperations.centerpointCoordinates(selected)
|
||||
const location = this.state.locationControl;
|
||||
const location = this.state.locationControl
|
||||
location.data.lon = centerpoint[0]
|
||||
location.data.lat = centerpoint[1]
|
||||
|
||||
const minZoom = Math.max(14, ...(this.state.layoutToUse?.layers?.map(l => l.minzoomVisible) ?? []))
|
||||
const minZoom = Math.max(
|
||||
14,
|
||||
...(this.state.layoutToUse?.layers?.map((l) => l.minzoomVisible) ?? [])
|
||||
)
|
||||
if (location.data.zoom < minZoom) {
|
||||
location.data.zoom = minZoom
|
||||
}
|
||||
|
||||
location.ping();
|
||||
|
||||
|
||||
location.ping()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,88 +1,87 @@
|
|||
import * as L from "leaflet";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import Constants from "../../Models/Constants";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import * as L from "leaflet"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import Constants from "../../Models/Constants"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
|
||||
/**
|
||||
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
|
||||
* Shows the given uiToShow-element in the messagebox
|
||||
*/
|
||||
export default class StrayClickHandler {
|
||||
private _lastMarker;
|
||||
private _lastMarker
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
LastClickLocation: UIEventSource<{ lat: number, lon: number }>,
|
||||
selectedElement: UIEventSource<string>,
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>,
|
||||
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
|
||||
selectedElement: UIEventSource<string>
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
leafletMap: UIEventSource<L.Map>
|
||||
},
|
||||
uiToShow: ScrollableFullScreen,
|
||||
iconToShow: BaseUIElement) {
|
||||
const self = this;
|
||||
iconToShow: BaseUIElement
|
||||
) {
|
||||
const self = this
|
||||
const leafletMap = state.leafletMap
|
||||
state.filteredLayers.data.forEach((filteredLayer) => {
|
||||
filteredLayer.isDisplayed.addCallback(isEnabled => {
|
||||
filteredLayer.isDisplayed.addCallback((isEnabled) => {
|
||||
if (isEnabled && self._lastMarker && leafletMap.data !== undefined) {
|
||||
// When a layer is activated, we remove the 'last click location' in order to force the user to reclick
|
||||
// This reclick might be at a location where a feature now appeared...
|
||||
state.leafletMap.data.removeLayer(self._lastMarker);
|
||||
state.leafletMap.data.removeLayer(self._lastMarker)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
state.LastClickLocation.addCallback(function (lastClick) {
|
||||
|
||||
if (self._lastMarker !== undefined) {
|
||||
state.leafletMap.data?.removeLayer(self._lastMarker);
|
||||
state.leafletMap.data?.removeLayer(self._lastMarker)
|
||||
}
|
||||
|
||||
if (lastClick === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
state.selectedElement.setData(undefined);
|
||||
state.selectedElement.setData(undefined)
|
||||
const clickCoor: [number, number] = [lastClick.lat, lastClick.lon]
|
||||
self._lastMarker = L.marker(clickCoor, {
|
||||
icon: L.divIcon({
|
||||
html: iconToShow.ConstructElement(),
|
||||
iconSize: [50, 50],
|
||||
iconAnchor: [25, 50],
|
||||
popupAnchor: [0, -45]
|
||||
})
|
||||
});
|
||||
popupAnchor: [0, -45],
|
||||
}),
|
||||
})
|
||||
const popup = L.popup({
|
||||
autoPan: true,
|
||||
autoPanPaddingTopLeft: [15, 15],
|
||||
closeOnEscapeKey: true,
|
||||
autoClose: true
|
||||
}).setContent("<div id='strayclick' style='height: 65vh'></div>");
|
||||
self._lastMarker.addTo(leafletMap.data);
|
||||
self._lastMarker.bindPopup(popup);
|
||||
autoClose: true,
|
||||
}).setContent("<div id='strayclick' style='height: 65vh'></div>")
|
||||
self._lastMarker.addTo(leafletMap.data)
|
||||
self._lastMarker.bindPopup(popup)
|
||||
|
||||
self._lastMarker.on("click", () => {
|
||||
if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) {
|
||||
self._lastMarker.closePopup()
|
||||
leafletMap.data.flyTo(clickCoor, Constants.userJourney.minZoomLevelToAddNewPoints)
|
||||
return;
|
||||
leafletMap.data.flyTo(
|
||||
clickCoor,
|
||||
Constants.userJourney.minZoomLevelToAddNewPoints
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
uiToShow.AttachTo("strayclick")
|
||||
uiToShow.Activate();
|
||||
});
|
||||
});
|
||||
uiToShow.Activate()
|
||||
})
|
||||
})
|
||||
|
||||
state.selectedElement.addCallback(() => {
|
||||
if (self._lastMarker !== undefined) {
|
||||
leafletMap.data.removeLayer(self._lastMarker);
|
||||
this._lastMarker = undefined;
|
||||
leafletMap.data.removeLayer(self._lastMarker)
|
||||
this._lastMarker = undefined
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import {Store, UIEventSource} from "../UIEventSource";
|
||||
import Locale from "../../UI/i18n/Locale";
|
||||
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer";
|
||||
import Combine from "../../UI/Base/Combine";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import {Utils} from "../../Utils";
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"
|
||||
import Combine from "../../UI/Base/Combine"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { ElementStorage } from "../ElementStorage"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export default class TitleHandler {
|
||||
constructor(state: {
|
||||
selectedElement: Store<any>,
|
||||
layoutToUse: LayoutConfig,
|
||||
selectedElement: Store<any>
|
||||
layoutToUse: LayoutConfig
|
||||
allElements: ElementStorage
|
||||
}) {
|
||||
const currentTitle: Store<string> = state.selectedElement.map(
|
||||
selected => {
|
||||
(selected) => {
|
||||
const layout = state.layoutToUse
|
||||
const defaultTitle = layout?.title?.txt ?? "MapComplete"
|
||||
|
||||
|
@ -21,27 +21,32 @@ export default class TitleHandler {
|
|||
return defaultTitle
|
||||
}
|
||||
|
||||
const tags = selected.properties;
|
||||
const tags = selected.properties
|
||||
for (const layer of layout.layers) {
|
||||
if (layer.title === undefined) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||
const tagsSource = state.allElements.getEventSourceById(tags.id) ?? new UIEventSource<any>(tags)
|
||||
const tagsSource =
|
||||
state.allElements.getEventSourceById(tags.id) ??
|
||||
new UIEventSource<any>(tags)
|
||||
const title = new TagRenderingAnswer(tagsSource, layer.title, {})
|
||||
return new Combine([defaultTitle, " | ", title]).ConstructElement()?.textContent ?? defaultTitle;
|
||||
return (
|
||||
new Combine([defaultTitle, " | ", title]).ConstructElement()
|
||||
?.textContent ?? defaultTitle
|
||||
)
|
||||
}
|
||||
}
|
||||
return defaultTitle
|
||||
}, [Locale.language]
|
||||
},
|
||||
[Locale.language]
|
||||
)
|
||||
|
||||
|
||||
currentTitle.addCallbackAndRunD(title => {
|
||||
currentTitle.addCallbackAndRunD((title) => {
|
||||
if (Utils.runningFromConsole) {
|
||||
return
|
||||
}
|
||||
document.title = title
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
172
Logic/BBox.ts
172
Logic/BBox.ts
|
@ -1,31 +1,32 @@
|
|||
import * as turf from "@turf/turf";
|
||||
import {TileRange, Tiles} from "../Models/TileRange";
|
||||
import {GeoOperations} from "./GeoOperations";
|
||||
import * as turf from "@turf/turf"
|
||||
import { TileRange, Tiles } from "../Models/TileRange"
|
||||
import { GeoOperations } from "./GeoOperations"
|
||||
|
||||
export class BBox {
|
||||
|
||||
static global: BBox = new BBox([[-180, -90], [180, 90]]);
|
||||
readonly maxLat: number;
|
||||
readonly maxLon: number;
|
||||
readonly minLat: number;
|
||||
readonly minLon: number;
|
||||
static global: BBox = new BBox([
|
||||
[-180, -90],
|
||||
[180, 90],
|
||||
])
|
||||
readonly maxLat: number
|
||||
readonly maxLon: number
|
||||
readonly minLat: number
|
||||
readonly minLon: number
|
||||
|
||||
/***
|
||||
* Coordinates should be [[lon, lat],[lon, lat]]
|
||||
* @param coordinates
|
||||
*/
|
||||
constructor(coordinates) {
|
||||
this.maxLat = -90;
|
||||
this.maxLon = -180;
|
||||
this.minLat = 90;
|
||||
this.minLon = 180;
|
||||
|
||||
this.maxLat = -90
|
||||
this.maxLon = -180
|
||||
this.minLat = 90
|
||||
this.minLon = 180
|
||||
|
||||
for (const coordinate of coordinates) {
|
||||
this.maxLon = Math.max(this.maxLon, coordinate[0]);
|
||||
this.maxLat = Math.max(this.maxLat, coordinate[1]);
|
||||
this.minLon = Math.min(this.minLon, coordinate[0]);
|
||||
this.minLat = Math.min(this.minLat, coordinate[1]);
|
||||
this.maxLon = Math.max(this.maxLon, coordinate[0])
|
||||
this.maxLat = Math.max(this.maxLat, coordinate[1])
|
||||
this.minLon = Math.min(this.minLon, coordinate[0])
|
||||
this.minLat = Math.min(this.minLat, coordinate[1])
|
||||
}
|
||||
|
||||
this.maxLon = Math.min(this.maxLon, 180)
|
||||
|
@ -33,27 +34,32 @@ export class BBox {
|
|||
this.minLon = Math.max(this.minLon, -180)
|
||||
this.minLat = Math.max(this.minLat, -90)
|
||||
|
||||
|
||||
this.check();
|
||||
this.check()
|
||||
}
|
||||
|
||||
static fromLeafletBounds(bounds) {
|
||||
return new BBox([[bounds.getWest(), bounds.getNorth()], [bounds.getEast(), bounds.getSouth()]])
|
||||
return new BBox([
|
||||
[bounds.getWest(), bounds.getNorth()],
|
||||
[bounds.getEast(), bounds.getSouth()],
|
||||
])
|
||||
}
|
||||
|
||||
static get(feature): BBox {
|
||||
if (feature.bbox?.overlapsWith === undefined) {
|
||||
const turfBbox: number[] = turf.bbox(feature)
|
||||
feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]);
|
||||
feature.bbox = new BBox([
|
||||
[turfBbox[0], turfBbox[1]],
|
||||
[turfBbox[2], turfBbox[3]],
|
||||
])
|
||||
}
|
||||
return feature.bbox;
|
||||
return feature.bbox
|
||||
}
|
||||
|
||||
static bboxAroundAll(bboxes: BBox[]): BBox {
|
||||
let maxLat: number = -90;
|
||||
let maxLon: number = -180;
|
||||
let minLat: number = 80;
|
||||
let minLon: number = 180;
|
||||
let maxLat: number = -90
|
||||
let maxLon: number = -180
|
||||
let minLat: number = 80
|
||||
let minLon: number = 180
|
||||
|
||||
for (const bbox of bboxes) {
|
||||
maxLat = Math.max(maxLat, bbox.maxLat)
|
||||
|
@ -61,17 +67,20 @@ export class BBox {
|
|||
minLat = Math.min(minLat, bbox.minLat)
|
||||
minLon = Math.min(minLon, bbox.minLon)
|
||||
}
|
||||
return new BBox([[maxLon, maxLat], [minLon, minLat]])
|
||||
return new BBox([
|
||||
[maxLon, maxLat],
|
||||
[minLon, minLat],
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the BBox based on a slippy map tile number
|
||||
*
|
||||
*
|
||||
* const bbox = BBox.fromTile(16, 32754, 21785)
|
||||
* bbox.minLon // => -0.076904296875
|
||||
* bbox.maxLon // => -0.0714111328125
|
||||
* bbox.minLat // => 51.5292513551899
|
||||
* bbox.maxLat // => 51.53266860674158
|
||||
* bbox.minLon // => -0.076904296875
|
||||
* bbox.maxLon // => -0.0714111328125
|
||||
* bbox.minLat // => 51.5292513551899
|
||||
* bbox.maxLat // => 51.53266860674158
|
||||
*/
|
||||
static fromTile(z: number, x: number, y: number): BBox {
|
||||
return new BBox(Tiles.tile_bounds_lon_lat(z, x, y))
|
||||
|
@ -85,11 +94,10 @@ export class BBox {
|
|||
}
|
||||
|
||||
public unionWith(other: BBox) {
|
||||
return new BBox([[
|
||||
Math.max(this.maxLon, other.maxLon),
|
||||
Math.max(this.maxLat, other.maxLat)],
|
||||
[Math.min(this.minLon, other.minLon),
|
||||
Math.min(this.minLat, other.minLat)]])
|
||||
return new BBox([
|
||||
[Math.max(this.maxLon, other.maxLon), Math.max(this.maxLat, other.maxLat)],
|
||||
[Math.min(this.minLon, other.minLon), Math.min(this.minLat, other.minLat)],
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,32 +110,31 @@ export class BBox {
|
|||
|
||||
public overlapsWith(other: BBox) {
|
||||
if (this.maxLon < other.minLon) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (this.maxLat < other.minLat) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (this.minLon > other.maxLon) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
return this.minLat <= other.maxLat;
|
||||
|
||||
return this.minLat <= other.maxLat
|
||||
}
|
||||
|
||||
public isContainedIn(other: BBox) {
|
||||
if (this.maxLon > other.maxLon) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (this.maxLat > other.maxLat) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (this.minLon < other.minLon) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (this.minLat < other.minLat) {
|
||||
return false
|
||||
}
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
getEast() {
|
||||
|
@ -147,32 +154,35 @@ export class BBox {
|
|||
}
|
||||
|
||||
contains(lonLat: [number, number]) {
|
||||
return this.minLat <= lonLat[1] && lonLat[1] <= this.maxLat
|
||||
&& this.minLon <= lonLat[0] && lonLat[0] <= this.maxLon
|
||||
return (
|
||||
this.minLat <= lonLat[1] &&
|
||||
lonLat[1] <= this.maxLat &&
|
||||
this.minLon <= lonLat[0] &&
|
||||
lonLat[0] <= this.maxLon
|
||||
)
|
||||
}
|
||||
|
||||
pad(factor: number, maxIncrease = 2): BBox {
|
||||
|
||||
const latDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLat - this.minLat) * factor)
|
||||
const lonDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor)
|
||||
return new BBox([[
|
||||
this.minLon - lonDiff,
|
||||
this.minLat - latDiff
|
||||
], [this.maxLon + lonDiff,
|
||||
this.maxLat + latDiff]])
|
||||
return new BBox([
|
||||
[this.minLon - lonDiff, this.minLat - latDiff],
|
||||
[this.maxLon + lonDiff, this.maxLat + latDiff],
|
||||
])
|
||||
}
|
||||
|
||||
padAbsolute(degrees: number): BBox {
|
||||
|
||||
return new BBox([[
|
||||
this.minLon - degrees,
|
||||
this.minLat - degrees
|
||||
], [this.maxLon + degrees,
|
||||
this.maxLat + degrees]])
|
||||
return new BBox([
|
||||
[this.minLon - degrees, this.minLat - degrees],
|
||||
[this.maxLon + degrees, this.maxLat + degrees],
|
||||
])
|
||||
}
|
||||
|
||||
toLeaflet() {
|
||||
return [[this.minLat, this.minLon], [this.maxLat, this.maxLon]]
|
||||
return [
|
||||
[this.minLat, this.minLon],
|
||||
[this.maxLat, this.maxLon],
|
||||
]
|
||||
}
|
||||
|
||||
asGeoJson(properties: any): any {
|
||||
|
@ -181,16 +191,16 @@ export class BBox {
|
|||
properties: properties,
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[
|
||||
|
||||
[this.minLon, this.minLat],
|
||||
[this.maxLon, this.minLat],
|
||||
[this.maxLon, this.maxLat],
|
||||
[this.minLon, this.maxLat],
|
||||
[this.minLon, this.minLat],
|
||||
|
||||
]]
|
||||
}
|
||||
coordinates: [
|
||||
[
|
||||
[this.minLon, this.minLat],
|
||||
[this.maxLon, this.minLat],
|
||||
[this.maxLon, this.maxLat],
|
||||
[this.minLon, this.maxLat],
|
||||
[this.minLon, this.minLat],
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -206,22 +216,22 @@ export class BBox {
|
|||
return new BBox([].concat(boundsul, boundslr))
|
||||
}
|
||||
|
||||
toMercator(): { minLat: number, maxLat: number, minLon: number, maxLon: number } {
|
||||
toMercator(): { minLat: number; maxLat: number; minLon: number; maxLon: number } {
|
||||
const [minLon, minLat] = GeoOperations.ConvertWgs84To900913([this.minLon, this.minLat])
|
||||
const [maxLon, maxLat] = GeoOperations.ConvertWgs84To900913([this.maxLon, this.maxLat])
|
||||
|
||||
return {
|
||||
minLon, maxLon,
|
||||
minLat, maxLat
|
||||
minLon,
|
||||
maxLon,
|
||||
minLat,
|
||||
maxLat,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private check() {
|
||||
private check() {
|
||||
if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) {
|
||||
console.trace("BBox with NaN detected:", this);
|
||||
throw "BBOX has NAN";
|
||||
console.trace("BBox with NaN detected:", this)
|
||||
throw "BBOX has NAN"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +1,56 @@
|
|||
/// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
|
||||
import {Store, UIEventSource} from "./UIEventSource";
|
||||
import FeaturePipeline from "./FeatureSource/FeaturePipeline";
|
||||
import Loc from "../Models/Loc";
|
||||
import {BBox} from "./BBox";
|
||||
import { Store, UIEventSource } from "./UIEventSource"
|
||||
import FeaturePipeline from "./FeatureSource/FeaturePipeline"
|
||||
import Loc from "../Models/Loc"
|
||||
import { BBox } from "./BBox"
|
||||
|
||||
export default class ContributorCount {
|
||||
public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource<
|
||||
Map<string, number>
|
||||
>(new Map<string, number>())
|
||||
private readonly state: {
|
||||
featurePipeline: FeaturePipeline
|
||||
currentBounds: Store<BBox>
|
||||
locationControl: Store<Loc>
|
||||
}
|
||||
private lastUpdate: Date = undefined
|
||||
|
||||
public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>());
|
||||
private readonly state: { featurePipeline: FeaturePipeline, currentBounds: Store<BBox>, locationControl: Store<Loc> };
|
||||
private lastUpdate: Date = undefined;
|
||||
|
||||
constructor(state: { featurePipeline: FeaturePipeline, currentBounds: Store<BBox>, locationControl: Store<Loc> }) {
|
||||
this.state = state;
|
||||
const self = this;
|
||||
state.currentBounds.map(bbox => {
|
||||
constructor(state: {
|
||||
featurePipeline: FeaturePipeline
|
||||
currentBounds: Store<BBox>
|
||||
locationControl: Store<Loc>
|
||||
}) {
|
||||
this.state = state
|
||||
const self = this
|
||||
state.currentBounds.map((bbox) => {
|
||||
self.update(bbox)
|
||||
})
|
||||
state.featurePipeline.runningQuery.addCallbackAndRun(
|
||||
_ => self.update(state.currentBounds.data)
|
||||
state.featurePipeline.runningQuery.addCallbackAndRun((_) =>
|
||||
self.update(state.currentBounds.data)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
private update(bbox: BBox) {
|
||||
if (bbox === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const now = new Date();
|
||||
if (this.lastUpdate !== undefined && ((now.getTime() - this.lastUpdate.getTime()) < 1000 * 60)) {
|
||||
return;
|
||||
const now = new Date()
|
||||
if (
|
||||
this.lastUpdate !== undefined &&
|
||||
now.getTime() - this.lastUpdate.getTime() < 1000 * 60
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.lastUpdate = now;
|
||||
this.lastUpdate = now
|
||||
const featuresList = this.state.featurePipeline.GetAllFeaturesWithin(bbox)
|
||||
const hist = new Map<string, number>();
|
||||
const hist = new Map<string, number>()
|
||||
for (const list of featuresList) {
|
||||
for (const feature of list) {
|
||||
const contributor = feature.properties["_last_edit:contributor"]
|
||||
const count = hist.get(contributor) ?? 0;
|
||||
const count = hist.get(contributor) ?? 0
|
||||
hist.set(contributor, count + 1)
|
||||
}
|
||||
}
|
||||
this.Contributors.setData(hist)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,37 @@
|
|||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||
import {QueryParameters} from "./Web/QueryParameters";
|
||||
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
|
||||
import {FixedUiElement} from "../UI/Base/FixedUiElement";
|
||||
import {Utils} from "../Utils";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import {SubtleButton} from "../UI/Base/SubtleButton";
|
||||
import BaseUIElement from "../UI/BaseUIElement";
|
||||
import {UIEventSource} from "./UIEventSource";
|
||||
import {LocalStorageSource} from "./Web/LocalStorageSource";
|
||||
import LZString from "lz-string";
|
||||
import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert";
|
||||
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
import SharedTagRenderings from "../Customizations/SharedTagRenderings";
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||
import { QueryParameters } from "./Web/QueryParameters"
|
||||
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
|
||||
import { FixedUiElement } from "../UI/Base/FixedUiElement"
|
||||
import { Utils } from "../Utils"
|
||||
import Combine from "../UI/Base/Combine"
|
||||
import { SubtleButton } from "../UI/Base/SubtleButton"
|
||||
import BaseUIElement from "../UI/BaseUIElement"
|
||||
import { UIEventSource } from "./UIEventSource"
|
||||
import { LocalStorageSource } from "./Web/LocalStorageSource"
|
||||
import LZString from "lz-string"
|
||||
import { FixLegacyTheme } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"
|
||||
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import SharedTagRenderings from "../Customizations/SharedTagRenderings"
|
||||
import * as known_layers from "../assets/generated/known_layers.json"
|
||||
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
|
||||
import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme"
|
||||
import * as licenses from "../assets/generated/license_info.json"
|
||||
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
|
||||
import {FixImages} from "../Models/ThemeConfig/Conversion/FixImages";
|
||||
import Svg from "../Svg";
|
||||
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages"
|
||||
import Svg from "../Svg"
|
||||
|
||||
export default class DetermineLayout {
|
||||
private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path))
|
||||
|
||||
private static readonly _knownImages =new Set( Array.from(licenses).map(l => l.path))
|
||||
|
||||
/**
|
||||
* Gets the correct layout for this website
|
||||
*/
|
||||
public static async GetLayout(): Promise<LayoutConfig> {
|
||||
|
||||
const loadCustomThemeParam = QueryParameters.GetQueryParameter("userlayout", "false", "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme")
|
||||
const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data);
|
||||
const loadCustomThemeParam = QueryParameters.GetQueryParameter(
|
||||
"userlayout",
|
||||
"false",
|
||||
"If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme"
|
||||
)
|
||||
const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data)
|
||||
|
||||
if (layoutFromBase64.startsWith("http")) {
|
||||
return await DetermineLayout.LoadRemoteTheme(layoutFromBase64)
|
||||
|
@ -42,150 +44,164 @@ export default class DetermineLayout {
|
|||
|
||||
let layoutId: string = undefined
|
||||
|
||||
const path = window.location.pathname.split("/").slice(-1)[0];
|
||||
const path = window.location.pathname.split("/").slice(-1)[0]
|
||||
if (path !== "theme.html" && path !== "") {
|
||||
layoutId = path;
|
||||
layoutId = path
|
||||
if (path.endsWith(".html")) {
|
||||
layoutId = path.substr(0, path.length - 5);
|
||||
layoutId = path.substr(0, path.length - 5)
|
||||
}
|
||||
console.log("Using layout", layoutId);
|
||||
console.log("Using layout", layoutId)
|
||||
}
|
||||
layoutId = QueryParameters.GetQueryParameter("layout", layoutId, "The layout to load into MapComplete").data;
|
||||
layoutId = QueryParameters.GetQueryParameter(
|
||||
"layout",
|
||||
layoutId,
|
||||
"The layout to load into MapComplete"
|
||||
).data
|
||||
return AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase())
|
||||
}
|
||||
|
||||
public static LoadLayoutFromHash(
|
||||
userLayoutParam: UIEventSource<string>
|
||||
): LayoutConfig | null {
|
||||
let hash = location.hash.substr(1);
|
||||
let json: any;
|
||||
public static LoadLayoutFromHash(userLayoutParam: UIEventSource<string>): LayoutConfig | null {
|
||||
let hash = location.hash.substr(1)
|
||||
let json: any
|
||||
|
||||
try {
|
||||
// layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
|
||||
const dedicatedHashFromLocalStorage = LocalStorageSource.Get(
|
||||
"user-layout-" + userLayoutParam.data?.replace(" ", "_")
|
||||
);
|
||||
)
|
||||
if (dedicatedHashFromLocalStorage.data?.length < 10) {
|
||||
dedicatedHashFromLocalStorage.setData(undefined);
|
||||
dedicatedHashFromLocalStorage.setData(undefined)
|
||||
}
|
||||
|
||||
const hashFromLocalStorage = LocalStorageSource.Get(
|
||||
"last-loaded-user-layout"
|
||||
);
|
||||
const hashFromLocalStorage = LocalStorageSource.Get("last-loaded-user-layout")
|
||||
if (hash.length < 10) {
|
||||
hash =
|
||||
dedicatedHashFromLocalStorage.data ??
|
||||
hashFromLocalStorage.data;
|
||||
hash = dedicatedHashFromLocalStorage.data ?? hashFromLocalStorage.data
|
||||
} else {
|
||||
console.log("Saving hash to local storage");
|
||||
hashFromLocalStorage.setData(hash);
|
||||
dedicatedHashFromLocalStorage.setData(hash);
|
||||
console.log("Saving hash to local storage")
|
||||
hashFromLocalStorage.setData(hash)
|
||||
dedicatedHashFromLocalStorage.setData(hash)
|
||||
}
|
||||
|
||||
try {
|
||||
json = JSON.parse(atob(hash));
|
||||
json = JSON.parse(atob(hash))
|
||||
} catch (e) {
|
||||
// We try to decode with lz-string
|
||||
try {
|
||||
json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash)))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON"))
|
||||
return null;
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
"Could not decode the hash",
|
||||
new FixedUiElement("Not a valid (LZ-compressed) JSON")
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const layoutToUse = DetermineLayout.prepCustomTheme(json)
|
||||
userLayoutParam.setData(layoutToUse.id);
|
||||
userLayoutParam.setData(layoutToUse.id)
|
||||
return layoutToUse
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
if (hash === undefined || hash.length < 10) {
|
||||
DetermineLayout.ShowErrorOnCustomTheme("Could not load a theme from the hash", new FixedUiElement("Hash does not contain data"), json)
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
"Could not load a theme from the hash",
|
||||
new FixedUiElement("Hash does not contain data"),
|
||||
json
|
||||
)
|
||||
}
|
||||
this.ShowErrorOnCustomTheme("Could not parse the hash", new FixedUiElement(e), json)
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
public static ShowErrorOnCustomTheme(
|
||||
intro: string = "Error: could not parse the custom layout:",
|
||||
error: BaseUIElement,
|
||||
json?: any) {
|
||||
json?: any
|
||||
) {
|
||||
new Combine([
|
||||
intro,
|
||||
error.SetClass("alert"),
|
||||
new SubtleButton(Svg.back_svg(),
|
||||
"Go back to the theme overview",
|
||||
{url: window.location.protocol + "//" + window.location.host + "/index.html", newTab: false}),
|
||||
json !== undefined ? new SubtleButton(Svg.download_svg(),"Download the JSON file").onClick(() => {
|
||||
Utils.offerContentsAsDownloadableFile(JSON.stringify(json, null, " "), "theme_definition.json")
|
||||
}) : undefined
|
||||
new SubtleButton(Svg.back_svg(), "Go back to the theme overview", {
|
||||
url: window.location.protocol + "//" + window.location.host + "/index.html",
|
||||
newTab: false,
|
||||
}),
|
||||
json !== undefined
|
||||
? new SubtleButton(Svg.download_svg(), "Download the JSON file").onClick(() => {
|
||||
Utils.offerContentsAsDownloadableFile(
|
||||
JSON.stringify(json, null, " "),
|
||||
"theme_definition.json"
|
||||
)
|
||||
})
|
||||
: undefined,
|
||||
])
|
||||
.SetClass("flex flex-col clickable")
|
||||
.AttachTo("centermessage");
|
||||
.AttachTo("centermessage")
|
||||
}
|
||||
|
||||
private static prepCustomTheme(json: any, sourceUrl?: string, forceId?: string): LayoutConfig {
|
||||
|
||||
if(json.layers === undefined && json.tagRenderings !== undefined){
|
||||
const iconTr = json.mapRendering.map(mr => mr.icon).find(icon => icon !== undefined)
|
||||
if (json.layers === undefined && json.tagRenderings !== undefined) {
|
||||
const iconTr = json.mapRendering.map((mr) => mr.icon).find((icon) => icon !== undefined)
|
||||
const icon = new TagRenderingConfig(iconTr).render.txt
|
||||
json = {
|
||||
id: json.id,
|
||||
description: json.description,
|
||||
descriptionTail: {
|
||||
en: "<div class='alert'>Layer only mode.</div> The loaded custom theme actually isn't a custom theme, but only contains a layer."
|
||||
en: "<div class='alert'>Layer only mode.</div> The loaded custom theme actually isn't a custom theme, but only contains a layer.",
|
||||
},
|
||||
icon,
|
||||
title: json.name,
|
||||
layers: [json],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const knownLayersDict = new Map<string, LayerConfigJson>()
|
||||
for (const key in known_layers.layers) {
|
||||
const layer = known_layers.layers[key]
|
||||
knownLayersDict.set(layer.id,<LayerConfigJson> layer)
|
||||
knownLayersDict.set(layer.id, <LayerConfigJson>layer)
|
||||
}
|
||||
const converState = {
|
||||
tagRenderings: SharedTagRenderings.SharedTagRenderingJson,
|
||||
sharedLayers: knownLayersDict,
|
||||
publicLayers: new Set<string>()
|
||||
publicLayers: new Set<string>(),
|
||||
}
|
||||
json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme")
|
||||
const raw = json;
|
||||
const raw = json
|
||||
|
||||
json = new FixImages(DetermineLayout._knownImages).convertStrict(json, "While fixing the images")
|
||||
json.enableNoteImports = json.enableNoteImports ?? false;
|
||||
json = new FixImages(DetermineLayout._knownImages).convertStrict(
|
||||
json,
|
||||
"While fixing the images"
|
||||
)
|
||||
json.enableNoteImports = json.enableNoteImports ?? false
|
||||
json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme")
|
||||
console.log("The layoutconfig is ", json)
|
||||
|
||||
|
||||
json.id = forceId ?? json.id
|
||||
|
||||
|
||||
return new LayoutConfig(json, false, {
|
||||
definitionRaw: JSON.stringify(raw, null, " "),
|
||||
definedAtUrl: sourceUrl
|
||||
definedAtUrl: sourceUrl,
|
||||
})
|
||||
}
|
||||
|
||||
private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> {
|
||||
console.log("Downloading map theme from ", link);
|
||||
console.log("Downloading map theme from ", link)
|
||||
|
||||
new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`)
|
||||
.AttachTo("centermessage");
|
||||
new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`).AttachTo(
|
||||
"centermessage"
|
||||
)
|
||||
|
||||
try {
|
||||
|
||||
let parsed = await Utils.downloadJson(link)
|
||||
try {
|
||||
let forcedId = parsed.id
|
||||
const url = new URL(link)
|
||||
if(!(url.hostname === "localhost" || url.hostname === "127.0.0.1")){
|
||||
forcedId = link;
|
||||
if (!(url.hostname === "localhost" || url.hostname === "127.0.0.1")) {
|
||||
forcedId = link
|
||||
}
|
||||
console.log("Loaded remote link:", link)
|
||||
return DetermineLayout.prepCustomTheme(parsed, link, forcedId);
|
||||
return DetermineLayout.prepCustomTheme(parsed, link, forcedId)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
|
@ -193,17 +209,15 @@ export default class DetermineLayout {
|
|||
new FixedUiElement(e),
|
||||
parsed
|
||||
)
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
`<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
|
||||
new FixedUiElement(e)
|
||||
)
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
/**
|
||||
* Keeps track of a dictionary 'elementID' -> UIEventSource<tags>
|
||||
*/
|
||||
import {UIEventSource} from "./UIEventSource";
|
||||
import {GeoJSONObject} from "@turf/turf";
|
||||
import { UIEventSource } from "./UIEventSource"
|
||||
import { GeoJSONObject } from "@turf/turf"
|
||||
|
||||
export class ElementStorage {
|
||||
public ContainingFeatures = new Map<string, any>()
|
||||
private _elements = new Map<string, UIEventSource<any>>()
|
||||
|
||||
public ContainingFeatures = new Map<string, any>();
|
||||
private _elements = new Map<string, UIEventSource<any>>();
|
||||
|
||||
constructor() {
|
||||
|
||||
}
|
||||
constructor() {}
|
||||
|
||||
addElementById(id: string, eventSource: UIEventSource<any>) {
|
||||
this._elements.set(id, eventSource);
|
||||
this._elements.set(id, eventSource)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -24,8 +21,8 @@ export class ElementStorage {
|
|||
* Note: it will cleverly merge the tags, if needed
|
||||
*/
|
||||
addOrGetElement(feature: any): UIEventSource<any> {
|
||||
const elementId = feature.properties.id;
|
||||
const newProperties = feature.properties;
|
||||
const elementId = feature.properties.id
|
||||
const newProperties = feature.properties
|
||||
|
||||
const es = this.addOrGetById(elementId, newProperties)
|
||||
|
||||
|
@ -33,91 +30,89 @@ export class ElementStorage {
|
|||
feature.properties = es.data
|
||||
|
||||
if (!this.ContainingFeatures.has(elementId)) {
|
||||
this.ContainingFeatures.set(elementId, feature);
|
||||
this.ContainingFeatures.set(elementId, feature)
|
||||
}
|
||||
|
||||
return es;
|
||||
return es
|
||||
}
|
||||
|
||||
getEventSourceById(elementId): UIEventSource<any> {
|
||||
if (elementId === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return this._elements.get(elementId);
|
||||
return this._elements.get(elementId)
|
||||
}
|
||||
|
||||
has(id) {
|
||||
return this._elements.has(id);
|
||||
return this._elements.has(id)
|
||||
}
|
||||
|
||||
addAlias(oldId: string, newId: string){
|
||||
addAlias(oldId: string, newId: string) {
|
||||
if (newId === undefined) {
|
||||
// We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
|
||||
const element = this.getEventSourceById(oldId);
|
||||
const element = this.getEventSourceById(oldId)
|
||||
element.data._deleted = "yes"
|
||||
element.ping();
|
||||
return;
|
||||
element.ping()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (oldId == newId) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
const element = this.getEventSourceById( oldId);
|
||||
const element = this.getEventSourceById(oldId)
|
||||
if (element === undefined) {
|
||||
// Element to rewrite not found, probably a node or relation that is not rendered
|
||||
return undefined
|
||||
}
|
||||
element.data.id = newId;
|
||||
this.addElementById(newId, element);
|
||||
this.ContainingFeatures.set(newId, this.ContainingFeatures.get( oldId))
|
||||
element.ping();
|
||||
element.data.id = newId
|
||||
this.addElementById(newId, element)
|
||||
this.ContainingFeatures.set(newId, this.ContainingFeatures.get(oldId))
|
||||
element.ping()
|
||||
}
|
||||
|
||||
|
||||
private addOrGetById(elementId: string, newProperties: any): UIEventSource<any> {
|
||||
if (!this._elements.has(elementId)) {
|
||||
const eventSource = new UIEventSource<any>(newProperties, "tags of " + elementId);
|
||||
this._elements.set(elementId, eventSource);
|
||||
return eventSource;
|
||||
const eventSource = new UIEventSource<any>(newProperties, "tags of " + elementId)
|
||||
this._elements.set(elementId, eventSource)
|
||||
return eventSource
|
||||
}
|
||||
|
||||
|
||||
const es = this._elements.get(elementId);
|
||||
const es = this._elements.get(elementId)
|
||||
if (es.data == newProperties) {
|
||||
// Reference comparison gives the same object! we can just return the event source
|
||||
return es;
|
||||
return es
|
||||
}
|
||||
const keptKeys = es.data;
|
||||
const keptKeys = es.data
|
||||
// The element already exists
|
||||
// We use the new feature to overwrite all the properties in the already existing eventsource
|
||||
const debug_msg = []
|
||||
let somethingChanged = false;
|
||||
let somethingChanged = false
|
||||
for (const k in newProperties) {
|
||||
if (!newProperties.hasOwnProperty(k)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
const v = newProperties[k];
|
||||
const v = newProperties[k]
|
||||
|
||||
if (keptKeys[k] !== v) {
|
||||
|
||||
if (v === undefined) {
|
||||
// The new value is undefined; the tag might have been removed
|
||||
// It might be a metatag as well
|
||||
// In the latter case, we do keep the tag!
|
||||
if (!k.startsWith("_")) {
|
||||
delete keptKeys[k]
|
||||
debug_msg.push(("Erased " + k))
|
||||
debug_msg.push("Erased " + k)
|
||||
}
|
||||
} else {
|
||||
keptKeys[k] = v;
|
||||
keptKeys[k] = v
|
||||
debug_msg.push(k + " --> " + v)
|
||||
}
|
||||
|
||||
somethingChanged = true;
|
||||
somethingChanged = true
|
||||
}
|
||||
}
|
||||
if (somethingChanged) {
|
||||
es.ping();
|
||||
es.ping()
|
||||
}
|
||||
return es;
|
||||
return es
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import {GeoOperations} from "./GeoOperations";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import RelationsTracker from "./Osm/RelationsTracker";
|
||||
import BaseUIElement from "../UI/BaseUIElement";
|
||||
import List from "../UI/Base/List";
|
||||
import Title from "../UI/Base/Title";
|
||||
import {BBox} from "./BBox";
|
||||
import {Feature, Geometry, MultiPolygon, Polygon} from "@turf/turf";
|
||||
import { GeoOperations } from "./GeoOperations"
|
||||
import Combine from "../UI/Base/Combine"
|
||||
import RelationsTracker from "./Osm/RelationsTracker"
|
||||
import BaseUIElement from "../UI/BaseUIElement"
|
||||
import List from "../UI/Base/List"
|
||||
import Title from "../UI/Base/Title"
|
||||
import { BBox } from "./BBox"
|
||||
import { Feature, Geometry, MultiPolygon, Polygon } from "@turf/turf"
|
||||
|
||||
export interface ExtraFuncParams {
|
||||
/**
|
||||
|
@ -13,7 +13,7 @@ export interface ExtraFuncParams {
|
|||
* Note that more features then requested can be given back.
|
||||
* Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...]
|
||||
*/
|
||||
getFeaturesWithin: (layerId: string, bbox: BBox) => Feature<Geometry, { id: string }>[][],
|
||||
getFeaturesWithin: (layerId: string, bbox: BBox) => Feature<Geometry, { id: string }>[][]
|
||||
memberships: RelationsTracker
|
||||
getFeatureById: (id: string) => Feature<Geometry, { id: string }>
|
||||
}
|
||||
|
@ -22,19 +22,23 @@ export interface ExtraFuncParams {
|
|||
* Describes a function that is added to a geojson object in order to calculate calculated tags
|
||||
*/
|
||||
interface ExtraFunction {
|
||||
readonly _name: string;
|
||||
readonly _args: string[];
|
||||
readonly _doc: string;
|
||||
readonly _f: (params: ExtraFuncParams, feat: Feature<Geometry, any>) => any;
|
||||
|
||||
readonly _name: string
|
||||
readonly _args: string[]
|
||||
readonly _doc: string
|
||||
readonly _f: (params: ExtraFuncParams, feat: Feature<Geometry, any>) => any
|
||||
}
|
||||
|
||||
class EnclosingFunc implements ExtraFunction {
|
||||
_name = "enclosingFeatures"
|
||||
_doc = ["Gives a list of all features in the specified layers which fully contain this object. Returned features will always be (multi)polygons. (LineStrings and Points from the other layers are ignored)", "",
|
||||
_doc = [
|
||||
"Gives a list of all features in the specified layers which fully contain this object. Returned features will always be (multi)polygons. (LineStrings and Points from the other layers are ignored)",
|
||||
"",
|
||||
"The result is a list of features: `{feat: Polygon}[]`",
|
||||
"This function will never return the feature itself."].join("\n")
|
||||
_args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"]
|
||||
"This function will never return the feature itself.",
|
||||
].join("\n")
|
||||
_args = [
|
||||
"...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)",
|
||||
]
|
||||
|
||||
_f(params: ExtraFuncParams, feat: Feature<Geometry, any>) {
|
||||
return (...layerIds: string[]) => {
|
||||
|
@ -45,10 +49,10 @@ class EnclosingFunc implements ExtraFunction {
|
|||
for (const layerId of layerIds) {
|
||||
const otherFeaturess = params.getFeaturesWithin(layerId, bbox)
|
||||
if (otherFeaturess === undefined) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (otherFeaturess.length === 0) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
for (const otherFeatures of otherFeaturess) {
|
||||
for (const otherFeature of otherFeatures) {
|
||||
|
@ -56,26 +60,33 @@ class EnclosingFunc implements ExtraFunction {
|
|||
continue
|
||||
}
|
||||
seenIds.add(otherFeature.properties.id)
|
||||
if (otherFeature.geometry.type !== "Polygon" && otherFeature.geometry.type !== "MultiPolygon") {
|
||||
continue;
|
||||
if (
|
||||
otherFeature.geometry.type !== "Polygon" &&
|
||||
otherFeature.geometry.type !== "MultiPolygon"
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (GeoOperations.completelyWithin(feat, <Feature<Polygon | MultiPolygon, any>>otherFeature)) {
|
||||
result.push({feat: otherFeature})
|
||||
if (
|
||||
GeoOperations.completelyWithin(
|
||||
feat,
|
||||
<Feature<Polygon | MultiPolygon, any>>otherFeature
|
||||
)
|
||||
) {
|
||||
result.push({ feat: otherFeature })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OverlapFunc implements ExtraFunction {
|
||||
|
||||
|
||||
_name = "overlapWith";
|
||||
_doc = ["Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well.",
|
||||
_name = "overlapWith"
|
||||
_doc = [
|
||||
"Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well.",
|
||||
"If the current feature is a point, all features that this point is embeded in are given.",
|
||||
"",
|
||||
"The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.",
|
||||
|
@ -83,27 +94,29 @@ class OverlapFunc implements ExtraFunction {
|
|||
"",
|
||||
"For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`",
|
||||
"",
|
||||
"Also see [enclosingFeatures](#enclosingFeatures) which can be used to get all objects which fully contain this feature"
|
||||
"Also see [enclosingFeatures](#enclosingFeatures) which can be used to get all objects which fully contain this feature",
|
||||
].join("\n")
|
||||
_args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"]
|
||||
_args = [
|
||||
"...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)",
|
||||
]
|
||||
|
||||
_f(params, feat) {
|
||||
return (...layerIds: string[]) => {
|
||||
const result: { feat: any, overlap: number }[] = []
|
||||
const result: { feat: any; overlap: number }[] = []
|
||||
const seenIds = new Set<string>()
|
||||
const bbox = BBox.get(feat)
|
||||
for (const layerId of layerIds) {
|
||||
const otherFeaturess = params.getFeaturesWithin(layerId, bbox)
|
||||
if (otherFeaturess === undefined) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (otherFeaturess.length === 0) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
for (const otherFeatures of otherFeaturess) {
|
||||
const overlap = GeoOperations.calculateOverlap(feat, otherFeatures)
|
||||
for (const overlappingFeature of overlap) {
|
||||
if(seenIds.has(overlappingFeature.feat.properties.id)){
|
||||
if (seenIds.has(overlappingFeature.feat.properties.id)) {
|
||||
continue
|
||||
}
|
||||
seenIds.add(overlappingFeature.feat.properties.id)
|
||||
|
@ -113,105 +126,113 @@ class OverlapFunc implements ExtraFunction {
|
|||
}
|
||||
|
||||
result.sort((a, b) => b.overlap - a.overlap)
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class IntersectionFunc implements ExtraFunction {
|
||||
|
||||
|
||||
_name = "intersectionsWith";
|
||||
_doc = "Gives the intersection points with selected features. Only works with (Multi)Polygons and LineStrings.\n\n" +
|
||||
_name = "intersectionsWith"
|
||||
_doc =
|
||||
"Gives the intersection points with selected features. Only works with (Multi)Polygons and LineStrings.\n\n" +
|
||||
"Returns a `{feat: GeoJson, intersections: [number,number][]}` where `feat` is the full, original feature. This list is in random order.\n\n" +
|
||||
"If the current feature is a point, this function will return an empty list.\n" +
|
||||
"Points from other layers are ignored - even if the points are parts of the current linestring."
|
||||
_args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for intersection)"]
|
||||
_args = [
|
||||
"...layerIds - one or more layer ids of the layer from which every feature is checked for intersection)",
|
||||
]
|
||||
|
||||
_f(params: ExtraFuncParams, feat) {
|
||||
return (...layerIds: string[]) => {
|
||||
const result: { feat: any, intersections: [number, number][] }[] = []
|
||||
const result: { feat: any; intersections: [number, number][] }[] = []
|
||||
|
||||
const bbox = BBox.get(feat)
|
||||
|
||||
for (const layerId of layerIds) {
|
||||
const otherLayers = params.getFeaturesWithin(layerId, bbox)
|
||||
if (otherLayers === undefined) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (otherLayers.length === 0) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
for (const tile of otherLayers) {
|
||||
for (const otherFeature of tile) {
|
||||
|
||||
const intersections = GeoOperations.LineIntersections(feat, otherFeature)
|
||||
if (intersections.length === 0) {
|
||||
continue
|
||||
}
|
||||
result.push({feat: otherFeature, intersections})
|
||||
result.push({ feat: otherFeature, intersections })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DistanceToFunc implements ExtraFunction {
|
||||
|
||||
_name = "distanceTo";
|
||||
_doc = "Calculates the distance between the feature and a specified point in meter. The input should either be a pair of coordinates, a geojson feature or the ID of an object";
|
||||
_name = "distanceTo"
|
||||
_doc =
|
||||
"Calculates the distance between the feature and a specified point in meter. The input should either be a pair of coordinates, a geojson feature or the ID of an object"
|
||||
_args = ["feature OR featureID OR longitude", "undefined OR latitude"]
|
||||
|
||||
_f(featuresPerLayer, feature) {
|
||||
return (arg0, lat) => {
|
||||
if (arg0 === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
if (typeof arg0 === "number") {
|
||||
// Feature._lon and ._lat is conveniently place by one of the other metatags
|
||||
return GeoOperations.distanceBetween([arg0, lat], GeoOperations.centerpointCoordinates(feature));
|
||||
return GeoOperations.distanceBetween(
|
||||
[arg0, lat],
|
||||
GeoOperations.centerpointCoordinates(feature)
|
||||
)
|
||||
}
|
||||
if (typeof arg0 === "string") {
|
||||
// This is an identifier
|
||||
const feature = featuresPerLayer.getFeatureById(arg0)
|
||||
if (feature === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
arg0 = feature;
|
||||
arg0 = feature
|
||||
}
|
||||
|
||||
// arg0 is probably a geojsonfeature
|
||||
return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0), GeoOperations.centerpointCoordinates(feature))
|
||||
|
||||
return GeoOperations.distanceBetween(
|
||||
GeoOperations.centerpointCoordinates(arg0),
|
||||
GeoOperations.centerpointCoordinates(feature)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ClosestObjectFunc implements ExtraFunction {
|
||||
_name = "closest"
|
||||
_doc = "Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. Returns a single geojson feature or undefined if nothing is found (or not yet loaded)"
|
||||
_doc =
|
||||
"Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. Returns a single geojson feature or undefined if nothing is found (or not yet loaded)"
|
||||
|
||||
_args = ["list of features or a layer name or '*' to get all features"]
|
||||
|
||||
_f(params, feature) {
|
||||
return (features) => ClosestNObjectFunc.GetClosestNFeatures(params, feature, features)?.[0]?.feat
|
||||
return (features) =>
|
||||
ClosestNObjectFunc.GetClosestNFeatures(params, feature, features)?.[0]?.feat
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class ClosestNObjectFunc implements ExtraFunction {
|
||||
_name = "closestn"
|
||||
_doc = "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature (excluding the feature itself). In the case of ways/polygons, only the centerpoint is considered. " +
|
||||
_doc =
|
||||
"Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature (excluding the feature itself). In the case of ways/polygons, only the centerpoint is considered. " +
|
||||
"Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet loaded)\n\n" +
|
||||
"If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)"
|
||||
_args = ["list of features or layer name or '*' to get all features", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"]
|
||||
_args = [
|
||||
"list of features or layer name or '*' to get all features",
|
||||
"amount of features",
|
||||
"unique tag key (optional)",
|
||||
"maxDistanceInMeters (optional)",
|
||||
]
|
||||
|
||||
/**
|
||||
* Gets the closes N features, sorted by ascending distance.
|
||||
|
@ -223,45 +244,61 @@ class ClosestNObjectFunc implements ExtraFunction {
|
|||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
static GetClosestNFeatures(params: ExtraFuncParams,
|
||||
feature: any,
|
||||
features: string | any[],
|
||||
options?: { maxFeatures?: number, uniqueTag?: string | undefined, maxDistance?: number }): { feat: any, distance: number }[] {
|
||||
static GetClosestNFeatures(
|
||||
params: ExtraFuncParams,
|
||||
feature: any,
|
||||
features: string | any[],
|
||||
options?: { maxFeatures?: number; uniqueTag?: string | undefined; maxDistance?: number }
|
||||
): { feat: any; distance: number }[] {
|
||||
const maxFeatures = options?.maxFeatures ?? 1
|
||||
const maxDistance = options?.maxDistance ?? 500
|
||||
const uniqueTag: string | undefined = options?.uniqueTag
|
||||
if (typeof features === "string") {
|
||||
const name = features
|
||||
const bbox = GeoOperations.bbox(GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance))
|
||||
const bbox = GeoOperations.bbox(
|
||||
GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance)
|
||||
)
|
||||
features = params.getFeaturesWithin(name, new BBox(bbox.geometry.coordinates))
|
||||
} else {
|
||||
features = [features]
|
||||
}
|
||||
if (features === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const selfCenter = GeoOperations.centerpointCoordinates(feature)
|
||||
let closestFeatures: { feat: any, distance: number }[] = [];
|
||||
let closestFeatures: { feat: any; distance: number }[] = []
|
||||
|
||||
for (const featureList of features) {
|
||||
// Features is provided by 'getFeaturesWithin' which returns a list of lists of features, hence the double loop here
|
||||
for (const otherFeature of featureList) {
|
||||
|
||||
if (otherFeature === feature || otherFeature.properties.id === feature.properties.id) {
|
||||
continue; // We ignore self
|
||||
if (
|
||||
otherFeature === feature ||
|
||||
otherFeature.properties.id === feature.properties.id
|
||||
) {
|
||||
continue // We ignore self
|
||||
}
|
||||
const distance = GeoOperations.distanceBetween(
|
||||
GeoOperations.centerpointCoordinates(otherFeature),
|
||||
selfCenter
|
||||
)
|
||||
if (distance === undefined || distance === null || isNaN(distance)) {
|
||||
console.error("Could not calculate the distance between", feature, "and", otherFeature)
|
||||
console.error(
|
||||
"Could not calculate the distance between",
|
||||
feature,
|
||||
"and",
|
||||
otherFeature
|
||||
)
|
||||
throw "Undefined distance!"
|
||||
}
|
||||
|
||||
if (distance === 0) {
|
||||
console.trace("Got a suspiciously zero distance between", otherFeature, "and self-feature", feature)
|
||||
console.trace(
|
||||
"Got a suspiciously zero distance between",
|
||||
otherFeature,
|
||||
"and self-feature",
|
||||
feature
|
||||
)
|
||||
}
|
||||
|
||||
if (distance > maxDistance) {
|
||||
|
@ -272,13 +309,15 @@ class ClosestNObjectFunc implements ExtraFunction {
|
|||
// This is the first matching feature we find - always add it
|
||||
closestFeatures.push({
|
||||
feat: otherFeature,
|
||||
distance: distance
|
||||
distance: distance,
|
||||
})
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) {
|
||||
if (
|
||||
closestFeatures.length >= maxFeatures &&
|
||||
closestFeatures[maxFeatures - 1].distance < distance
|
||||
) {
|
||||
// The last feature of the list (and thus the furthest away is still closer
|
||||
// No use for checking, as we already have plenty of features!
|
||||
continue
|
||||
|
@ -286,11 +325,13 @@ class ClosestNObjectFunc implements ExtraFunction {
|
|||
|
||||
let targetIndex = closestFeatures.length
|
||||
for (let i = 0; i < closestFeatures.length; i++) {
|
||||
const closestFeature = closestFeatures[i];
|
||||
const closestFeature = closestFeatures[i]
|
||||
|
||||
if (uniqueTag !== undefined) {
|
||||
const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined &&
|
||||
closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag]
|
||||
const uniqueTagsMatch =
|
||||
otherFeature.properties[uniqueTag] !== undefined &&
|
||||
closestFeature.feat.properties[uniqueTag] ===
|
||||
otherFeature.properties[uniqueTag]
|
||||
if (uniqueTagsMatch) {
|
||||
targetIndex = -1
|
||||
if (closestFeature.distance > distance) {
|
||||
|
@ -298,9 +339,9 @@ class ClosestNObjectFunc implements ExtraFunction {
|
|||
// We want to see the tag `uniquetag=some_value` only once in the entire list (e.g. to prevent road segements of identical names to fill up the list of 'names of nearby roads')
|
||||
// AT this point, we have found a closer segment with the same, identical tag
|
||||
// so we replace directly
|
||||
closestFeatures[i] = {feat: otherFeature, distance: distance}
|
||||
closestFeatures[i] = { feat: otherFeature, distance: distance }
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -316,19 +357,19 @@ class ClosestNObjectFunc implements ExtraFunction {
|
|||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (targetIndex == -1) {
|
||||
continue; // value is already swapped by the unique tag
|
||||
continue // value is already swapped by the unique tag
|
||||
}
|
||||
|
||||
if (targetIndex < maxFeatures) {
|
||||
// insert and drop one
|
||||
closestFeatures.splice(targetIndex, 0, {
|
||||
feat: otherFeature,
|
||||
distance: distance
|
||||
distance: distance,
|
||||
})
|
||||
if (closestFeatures.length >= maxFeatures) {
|
||||
closestFeatures.splice(maxFeatures, 1)
|
||||
|
@ -337,19 +378,15 @@ class ClosestNObjectFunc implements ExtraFunction {
|
|||
// Overwrite the last element
|
||||
closestFeatures[targetIndex] = {
|
||||
feat: otherFeature,
|
||||
distance: distance
|
||||
distance: distance,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
return closestFeatures;
|
||||
return closestFeatures
|
||||
}
|
||||
|
||||
_f(params, feature) {
|
||||
|
||||
return (features, amount, uniqueTag, maxDistanceInMeters) => {
|
||||
let distance: number = Number(maxDistanceInMeters)
|
||||
if (isNaN(distance)) {
|
||||
|
@ -358,60 +395,54 @@ class ClosestNObjectFunc implements ExtraFunction {
|
|||
return ClosestNObjectFunc.GetClosestNFeatures(params, feature, features, {
|
||||
maxFeatures: Number(amount),
|
||||
uniqueTag: uniqueTag,
|
||||
maxDistance: distance
|
||||
});
|
||||
maxDistance: distance,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class Memberships implements ExtraFunction {
|
||||
_name = "memberships"
|
||||
_doc = "Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of. " +
|
||||
_doc =
|
||||
"Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of. " +
|
||||
"\n\n" +
|
||||
"For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`"
|
||||
_args = []
|
||||
|
||||
_f(params, feat) {
|
||||
return () =>
|
||||
params.memberships.knownRelations.data.get(feat.properties.id) ?? []
|
||||
|
||||
return () => params.memberships.knownRelations.data.get(feat.properties.id) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class GetParsed implements ExtraFunction {
|
||||
_name = "get"
|
||||
_doc = "Gets the property of the feature, parses it (as JSON) and returns it. Might return 'undefined' if not defined, null, ..."
|
||||
_doc =
|
||||
"Gets the property of the feature, parses it (as JSON) and returns it. Might return 'undefined' if not defined, null, ..."
|
||||
_args = ["key"]
|
||||
|
||||
_f(params, feat) {
|
||||
return key => {
|
||||
return (key) => {
|
||||
const value = feat.properties[key]
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
if (parsed === null) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return parsed;
|
||||
return parsed
|
||||
} catch (e) {
|
||||
console.warn("Could not parse property " + key + " due to: " + e + ", the value is " + value)
|
||||
return undefined;
|
||||
console.warn(
|
||||
"Could not parse property " + key + " due to: " + e + ", the value is " + value
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ExtraFunctions {
|
||||
|
||||
|
||||
static readonly intro = new Combine([
|
||||
new Title("Calculating tags with Javascript", 2),
|
||||
"In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. `lat`, `lon`, `_country`), as detailed above.",
|
||||
|
@ -421,13 +452,13 @@ export class ExtraFunctions {
|
|||
new List([
|
||||
"DO NOT DO THIS AS BEGINNER",
|
||||
"**Only do this if all other techniques fail** This should _not_ be done to create a rendering effect, only to calculate a specific value",
|
||||
"**THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs."
|
||||
"**THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs.",
|
||||
]),
|
||||
"To enable this feature, add a field `calculatedTags` in the layer object, e.g.:",
|
||||
"````",
|
||||
"\"calculatedTags\": [",
|
||||
" \"_someKey=javascript-expression\",",
|
||||
" \"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator\",",
|
||||
'"calculatedTags": [',
|
||||
' "_someKey=javascript-expression",',
|
||||
' "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",',
|
||||
" \"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ",
|
||||
" ]",
|
||||
"````",
|
||||
|
@ -436,11 +467,12 @@ export class ExtraFunctions {
|
|||
|
||||
new List([
|
||||
"`area` contains the surface area (in square meters) of the object",
|
||||
"`lat` and `lon` contain the latitude and longitude"
|
||||
"`lat` and `lon` contain the latitude and longitude",
|
||||
]),
|
||||
"Some advanced functions are available on **feat** as well:"
|
||||
]).SetClass("flex-col").AsMarkdown();
|
||||
|
||||
"Some advanced functions are available on **feat** as well:",
|
||||
])
|
||||
.SetClass("flex-col")
|
||||
.AsMarkdown()
|
||||
|
||||
private static readonly allFuncs: ExtraFunction[] = [
|
||||
new DistanceToFunc(),
|
||||
|
@ -450,8 +482,8 @@ export class ExtraFunctions {
|
|||
new ClosestObjectFunc(),
|
||||
new ClosestNObjectFunc(),
|
||||
new Memberships(),
|
||||
new GetParsed()
|
||||
];
|
||||
new GetParsed(),
|
||||
]
|
||||
|
||||
public static FullPatchFeature(params: ExtraFuncParams, feature) {
|
||||
if (feature._is_patched) {
|
||||
|
@ -464,20 +496,15 @@ export class ExtraFunctions {
|
|||
}
|
||||
|
||||
public static HelpText(): BaseUIElement {
|
||||
|
||||
const elems = []
|
||||
for (const func of ExtraFunctions.allFuncs) {
|
||||
elems.push(new Title(func._name, 3),
|
||||
func._doc,
|
||||
new List(func._args ?? [], true))
|
||||
elems.push(new Title(func._name, 3), func._doc, new List(func._args ?? [], true))
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
ExtraFunctions.intro,
|
||||
new List(ExtraFunctions.allFuncs.map(func => `[${func._name}](#${func._name})`)),
|
||||
...elems
|
||||
]);
|
||||
new List(ExtraFunctions.allFuncs.map((func) => `[${func._name}](#${func._name})`)),
|
||||
...elems,
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,26 +1,30 @@
|
|||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import MetaTagging from "../../MetaTagging";
|
||||
import {ElementStorage} from "../../ElementStorage";
|
||||
import {ExtraFuncParams} from "../../ExtraFunctions";
|
||||
import FeaturePipeline from "../FeaturePipeline";
|
||||
import {BBox} from "../../BBox";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import MetaTagging from "../../MetaTagging"
|
||||
import { ElementStorage } from "../../ElementStorage"
|
||||
import { ExtraFuncParams } from "../../ExtraFunctions"
|
||||
import FeaturePipeline from "../FeaturePipeline"
|
||||
import { BBox } from "../../BBox"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
|
||||
/****
|
||||
* Concerned with the logic of updating the right layer at the right time
|
||||
*/
|
||||
class MetatagUpdater {
|
||||
public readonly neededLayerBboxes = new Map<string /*layerId*/, BBox>()
|
||||
private source: FeatureSourceForLayer & Tiled;
|
||||
private source: FeatureSourceForLayer & Tiled
|
||||
private readonly params: ExtraFuncParams
|
||||
private state: { allElements?: ElementStorage };
|
||||
private state: { allElements?: ElementStorage }
|
||||
|
||||
private readonly isDirty = new UIEventSource(false)
|
||||
|
||||
constructor(source: FeatureSourceForLayer & Tiled, state: { allElements?: ElementStorage }, featurePipeline: FeaturePipeline) {
|
||||
this.state = state;
|
||||
this.source = source;
|
||||
const self = this;
|
||||
constructor(
|
||||
source: FeatureSourceForLayer & Tiled,
|
||||
state: { allElements?: ElementStorage },
|
||||
featurePipeline: FeaturePipeline
|
||||
) {
|
||||
this.state = state
|
||||
this.source = source
|
||||
const self = this
|
||||
this.params = {
|
||||
getFeatureById(id) {
|
||||
return state.allElements.ContainingFeatures.get(id)
|
||||
|
@ -29,21 +33,20 @@ class MetatagUpdater {
|
|||
// We keep track of the BBOX that this source needs
|
||||
let oldBbox: BBox = self.neededLayerBboxes.get(layerId)
|
||||
if (oldBbox === undefined) {
|
||||
self.neededLayerBboxes.set(layerId, bbox);
|
||||
self.neededLayerBboxes.set(layerId, bbox)
|
||||
} else if (!bbox.isContainedIn(oldBbox)) {
|
||||
self.neededLayerBboxes.set(layerId, oldBbox.unionWith(bbox))
|
||||
}
|
||||
return featurePipeline.GetFeaturesWithin(layerId, bbox)
|
||||
},
|
||||
memberships: featurePipeline.relationTracker
|
||||
memberships: featurePipeline.relationTracker,
|
||||
}
|
||||
this.isDirty.stabilized(100).addCallback(dirty => {
|
||||
this.isDirty.stabilized(100).addCallback((dirty) => {
|
||||
if (dirty) {
|
||||
self.updateMetaTags()
|
||||
}
|
||||
})
|
||||
this.source.features.addCallbackAndRunD(_ => self.isDirty.setData(true))
|
||||
|
||||
this.source.features.addCallbackAndRunD((_) => self.isDirty.setData(true))
|
||||
}
|
||||
|
||||
public requestUpdate() {
|
||||
|
@ -57,56 +60,58 @@ class MetatagUpdater {
|
|||
this.isDirty.setData(false)
|
||||
return
|
||||
}
|
||||
MetaTagging.addMetatags(
|
||||
features,
|
||||
this.params,
|
||||
this.source.layer.layerDef,
|
||||
this.state)
|
||||
MetaTagging.addMetatags(features, this.params, this.source.layer.layerDef, this.state)
|
||||
this.isDirty.setData(false)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class MetaTagRecalculator {
|
||||
private _state: {
|
||||
allElements?: ElementStorage
|
||||
};
|
||||
private _featurePipeline: FeaturePipeline;
|
||||
private readonly _alreadyRegistered: Set<FeatureSourceForLayer & Tiled> = new Set<FeatureSourceForLayer & Tiled>()
|
||||
}
|
||||
private _featurePipeline: FeaturePipeline
|
||||
private readonly _alreadyRegistered: Set<FeatureSourceForLayer & Tiled> = new Set<
|
||||
FeatureSourceForLayer & Tiled
|
||||
>()
|
||||
private readonly _notifiers: MetatagUpdater[] = []
|
||||
|
||||
/**
|
||||
* The meta tag recalculator receives tiles of layers via the 'registerSource'-function.
|
||||
* It keeps track of which sources have had their share calculated, and which should be re-updated if some other data is loaded
|
||||
*/
|
||||
constructor(state: { allElements?: ElementStorage, currentView: FeatureSourceForLayer & Tiled }, featurePipeline: FeaturePipeline) {
|
||||
this._featurePipeline = featurePipeline;
|
||||
this._state = state;
|
||||
|
||||
if(state.currentView !== undefined){
|
||||
const currentViewUpdater = new MetatagUpdater(state.currentView, this._state, this._featurePipeline)
|
||||
this._alreadyRegistered.add(state.currentView)
|
||||
this._notifiers.push(currentViewUpdater)
|
||||
state.currentView.features.addCallback(_ => {
|
||||
console.debug("Requesting an update for currentView")
|
||||
currentViewUpdater.updateMetaTags();
|
||||
})
|
||||
}
|
||||
constructor(
|
||||
state: { allElements?: ElementStorage; currentView: FeatureSourceForLayer & Tiled },
|
||||
featurePipeline: FeaturePipeline
|
||||
) {
|
||||
this._featurePipeline = featurePipeline
|
||||
this._state = state
|
||||
|
||||
if (state.currentView !== undefined) {
|
||||
const currentViewUpdater = new MetatagUpdater(
|
||||
state.currentView,
|
||||
this._state,
|
||||
this._featurePipeline
|
||||
)
|
||||
this._alreadyRegistered.add(state.currentView)
|
||||
this._notifiers.push(currentViewUpdater)
|
||||
state.currentView.features.addCallback((_) => {
|
||||
console.debug("Requesting an update for currentView")
|
||||
currentViewUpdater.updateMetaTags()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public registerSource(source: FeatureSourceForLayer & Tiled, recalculateOnEveryChange = false) {
|
||||
if (source === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (this._alreadyRegistered.has(source)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this._alreadyRegistered.add(source)
|
||||
this._notifiers.push(new MetatagUpdater(source, this._state, this._featurePipeline))
|
||||
const self = this;
|
||||
source.features.addCallbackAndRunD(_ => {
|
||||
const self = this
|
||||
source.features.addCallbackAndRunD((_) => {
|
||||
const layerName = source.layer.layerDef.id
|
||||
for (const updater of self._notifiers) {
|
||||
const neededBbox = updater.neededLayerBboxes.get(layerName)
|
||||
|
@ -118,7 +123,5 @@ export default class MetaTagRecalculator {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
import FeatureSource from "../FeatureSource";
|
||||
import {Store} from "../../UIEventSource";
|
||||
import {ElementStorage} from "../../ElementStorage";
|
||||
import FeatureSource from "../FeatureSource"
|
||||
import { Store } from "../../UIEventSource"
|
||||
import { ElementStorage } from "../../ElementStorage"
|
||||
|
||||
/**
|
||||
* Makes sure that every feature is added to the ElementsStorage, so that the tags-eventsource can be retrieved
|
||||
*/
|
||||
export default class RegisteringAllFromFeatureSourceActor {
|
||||
public readonly features: Store<{ feature: any; freshness: Date }[]>;
|
||||
public readonly name;
|
||||
public readonly features: Store<{ feature: any; freshness: Date }[]>
|
||||
public readonly name
|
||||
|
||||
constructor(source: FeatureSource, allElements: ElementStorage) {
|
||||
this.features = source.features;
|
||||
this.name = "RegisteringSource of " + source.name;
|
||||
this.features.addCallbackAndRunD(features => {
|
||||
this.features = source.features
|
||||
this.name = "RegisteringSource of " + source.name
|
||||
this.features.addCallbackAndRunD((features) => {
|
||||
for (const feature of features) {
|
||||
allElements.addOrGetElement(feature.feature)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import FeatureSource, {Tiled} from "../FeatureSource";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {IdbLocalStorage} from "../../Web/IdbLocalStorage";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import {BBox} from "../../BBox";
|
||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import Loc from "../../../Models/Loc";
|
||||
import FeatureSource, { Tiled } from "../FeatureSource"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { IdbLocalStorage } from "../../Web/IdbLocalStorage"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { BBox } from "../../BBox"
|
||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import Loc from "../../../Models/Loc"
|
||||
|
||||
/***
|
||||
* Saves all the features that are passed in to localstorage, so they can be retrieved on the next run
|
||||
|
@ -15,20 +15,23 @@ import Loc from "../../../Models/Loc";
|
|||
*/
|
||||
export default class SaveTileToLocalStorageActor {
|
||||
private readonly visitedTiles: UIEventSource<Map<number, Date>>
|
||||
private readonly _layer: LayerConfig;
|
||||
private readonly _layer: LayerConfig
|
||||
private readonly _flayer: FilteredLayer
|
||||
private readonly initializeTime = new Date()
|
||||
|
||||
constructor(layer: FilteredLayer) {
|
||||
this._flayer = layer
|
||||
this._layer = layer.layerDef
|
||||
this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id,
|
||||
{defaultValue: new Map<number, Date>(),})
|
||||
this.visitedTiles.stabilized(100).addCallbackAndRunD(tiles => {
|
||||
this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, {
|
||||
defaultValue: new Map<number, Date>(),
|
||||
})
|
||||
this.visitedTiles.stabilized(100).addCallbackAndRunD((tiles) => {
|
||||
for (const key of Array.from(tiles.keys())) {
|
||||
const tileFreshness = tiles.get(key)
|
||||
|
||||
const toOld = (this.initializeTime.getTime() - tileFreshness.getTime()) > 1000 * this._layer.maxAgeOfCache
|
||||
const toOld =
|
||||
this.initializeTime.getTime() - tileFreshness.getTime() >
|
||||
1000 * this._layer.maxAgeOfCache
|
||||
if (toOld) {
|
||||
// Purge this tile
|
||||
this.SetIdb(key, undefined)
|
||||
|
@ -37,27 +40,28 @@ export default class SaveTileToLocalStorageActor {
|
|||
}
|
||||
}
|
||||
this.visitedTiles.ping()
|
||||
return true;
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
public LoadTilesFromDisk(currentBounds: UIEventSource<BBox>, location: UIEventSource<Loc>,
|
||||
registerFreshness: (tileId: number, freshness: Date) => void,
|
||||
registerTile: ((src: FeatureSource & Tiled) => void)) {
|
||||
const self = this;
|
||||
public LoadTilesFromDisk(
|
||||
currentBounds: UIEventSource<BBox>,
|
||||
location: UIEventSource<Loc>,
|
||||
registerFreshness: (tileId: number, freshness: Date) => void,
|
||||
registerTile: (src: FeatureSource & Tiled) => void
|
||||
) {
|
||||
const self = this
|
||||
const loadedTiles = new Set<number>()
|
||||
this.visitedTiles.addCallbackD(tiles => {
|
||||
this.visitedTiles.addCallbackD((tiles) => {
|
||||
if (tiles.size === 0) {
|
||||
// We don't do anything yet as probably not yet loaded from disk
|
||||
// We'll unregister later on
|
||||
return;
|
||||
return
|
||||
}
|
||||
currentBounds.addCallbackAndRunD(bbox => {
|
||||
|
||||
currentBounds.addCallbackAndRunD((bbox) => {
|
||||
if (self._layer.minzoomVisible > location.data.zoom) {
|
||||
// Not enough zoom
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// Iterate over all available keys in the local storage, check which are needed and fresh enough
|
||||
|
@ -71,32 +75,35 @@ export default class SaveTileToLocalStorageActor {
|
|||
registerFreshness(key, tileFreshness)
|
||||
const tileBbox = BBox.fromTileIndex(key)
|
||||
if (!bbox.overlapsWith(tileBbox)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (loadedTiles.has(key)) {
|
||||
// Already loaded earlier
|
||||
continue
|
||||
}
|
||||
loadedTiles.add(key)
|
||||
this.GetIdb(key).then((features: { feature: any, freshness: Date }[]) => {
|
||||
if(features === undefined){
|
||||
return;
|
||||
this.GetIdb(key).then((features: { feature: any; freshness: Date }[]) => {
|
||||
if (features === undefined) {
|
||||
return
|
||||
}
|
||||
console.debug("Loaded tile " + self._layer.id + "_" + key + " from disk")
|
||||
const src = new SimpleFeatureSource(self._flayer, key, new UIEventSource<{ feature: any; freshness: Date }[]>(features))
|
||||
const src = new SimpleFeatureSource(
|
||||
self._flayer,
|
||||
key,
|
||||
new UIEventSource<{ feature: any; freshness: Date }[]>(features)
|
||||
)
|
||||
registerTile(src)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return true; // Remove the callback
|
||||
|
||||
return true // Remove the callback
|
||||
})
|
||||
}
|
||||
|
||||
public addTile(tile: FeatureSource & Tiled) {
|
||||
const self = this
|
||||
tile.features.addCallbackAndRunD(features => {
|
||||
tile.features.addCallbackAndRunD((features) => {
|
||||
const now = new Date()
|
||||
|
||||
if (features.length > 0) {
|
||||
|
@ -109,11 +116,10 @@ export default class SaveTileToLocalStorageActor {
|
|||
|
||||
public poison(lon: number, lat: number) {
|
||||
for (let z = 0; z < 25; z++) {
|
||||
const {x, y} = Tiles.embedded_tile(lat, lon, z)
|
||||
const { x, y } = Tiles.embedded_tile(lat, lon, z)
|
||||
const tileId = Tiles.tile_index(z, x, y)
|
||||
this.visitedTiles.data.delete(tileId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public MarkVisited(tileId: number, freshness: Date) {
|
||||
|
@ -125,11 +131,18 @@ export default class SaveTileToLocalStorageActor {
|
|||
try {
|
||||
IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data)
|
||||
} catch (e) {
|
||||
console.error("Could not save tile to indexed-db: ", e, "tileIndex is:", tileIndex, "for layer", this._layer.id)
|
||||
console.error(
|
||||
"Could not save tile to indexed-db: ",
|
||||
e,
|
||||
"tileIndex is:",
|
||||
tileIndex,
|
||||
"for layer",
|
||||
this._layer.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private GetIdb(tileIndex) {
|
||||
return IdbLocalStorage.GetDirectly(this._layer.id + "_" + tileIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,33 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import FilteringFeatureSource from "./Sources/FilteringFeatureSource";
|
||||
import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "./FeatureSource";
|
||||
import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource";
|
||||
import {Store, UIEventSource} from "../UIEventSource";
|
||||
import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy";
|
||||
import RememberingSource from "./Sources/RememberingSource";
|
||||
import OverpassFeatureSource from "../Actors/OverpassFeatureSource";
|
||||
import GeoJsonSource from "./Sources/GeoJsonSource";
|
||||
import Loc from "../../Models/Loc";
|
||||
import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor";
|
||||
import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor";
|
||||
import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource";
|
||||
import {TileHierarchyMerger} from "./TiledFeatureSource/TileHierarchyMerger";
|
||||
import RelationsTracker from "../Osm/RelationsTracker";
|
||||
import {NewGeometryFromChangesFeatureSource} from "./Sources/NewGeometryFromChangesFeatureSource";
|
||||
import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator";
|
||||
import {BBox} from "../BBox";
|
||||
import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource";
|
||||
import {Tiles} from "../../Models/TileRange";
|
||||
import TileFreshnessCalculator from "./TileFreshnessCalculator";
|
||||
import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource";
|
||||
import MapState from "../State/MapState";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import {OsmFeature} from "../../Models/OsmFeature";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {FilterState} from "../../Models/FilteredLayer";
|
||||
import {GeoOperations} from "../GeoOperations";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import FilteringFeatureSource from "./Sources/FilteringFeatureSource"
|
||||
import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter"
|
||||
import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "./FeatureSource"
|
||||
import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { TileHierarchyTools } from "./TiledFeatureSource/TileHierarchy"
|
||||
import RememberingSource from "./Sources/RememberingSource"
|
||||
import OverpassFeatureSource from "../Actors/OverpassFeatureSource"
|
||||
import GeoJsonSource from "./Sources/GeoJsonSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor"
|
||||
import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor"
|
||||
import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource"
|
||||
import { TileHierarchyMerger } from "./TiledFeatureSource/TileHierarchyMerger"
|
||||
import RelationsTracker from "../Osm/RelationsTracker"
|
||||
import { NewGeometryFromChangesFeatureSource } from "./Sources/NewGeometryFromChangesFeatureSource"
|
||||
import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator"
|
||||
import { BBox } from "../BBox"
|
||||
import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
import TileFreshnessCalculator from "./TileFreshnessCalculator"
|
||||
import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import MapState from "../State/MapState"
|
||||
import { ElementStorage } from "../ElementStorage"
|
||||
import { OsmFeature } from "../../Models/OsmFeature"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { FilterState } from "../../Models/FilteredLayer"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
/**
|
||||
* The features pipeline ties together a myriad of various datasources:
|
||||
|
@ -42,12 +41,12 @@ import {Utils} from "../../Utils";
|
|||
*
|
||||
*/
|
||||
export default class FeaturePipeline {
|
||||
|
||||
public readonly sufficientlyZoomed: Store<boolean>;
|
||||
public readonly runningQuery: Store<boolean>;
|
||||
public readonly timeout: UIEventSource<number>;
|
||||
public readonly sufficientlyZoomed: Store<boolean>
|
||||
public readonly runningQuery: Store<boolean>
|
||||
public readonly timeout: UIEventSource<number>
|
||||
public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
public readonly newDataLoadedSignal: UIEventSource<FeatureSource> = new UIEventSource<FeatureSource>(undefined)
|
||||
public readonly newDataLoadedSignal: UIEventSource<FeatureSource> =
|
||||
new UIEventSource<FeatureSource>(undefined)
|
||||
public readonly relationTracker: RelationsTracker
|
||||
/**
|
||||
* Keeps track of all raw OSM-nodes.
|
||||
|
@ -55,19 +54,19 @@ export default class FeaturePipeline {
|
|||
*/
|
||||
public readonly fullNodeDatabase?: FullNodeDatabaseSource
|
||||
private readonly overpassUpdater: OverpassFeatureSource
|
||||
private state: MapState;
|
||||
private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>;
|
||||
private state: MapState
|
||||
private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>
|
||||
/**
|
||||
* Keeps track of the age of the loaded data.
|
||||
* Has one freshness-Calculator for every layer
|
||||
* @private
|
||||
*/
|
||||
private readonly freshnesses = new Map<string, TileFreshnessCalculator>();
|
||||
private readonly oldestAllowedDate: Date;
|
||||
private readonly freshnesses = new Map<string, TileFreshnessCalculator>()
|
||||
private readonly oldestAllowedDate: Date
|
||||
private readonly osmSourceZoomLevel
|
||||
private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>()
|
||||
|
||||
private readonly newGeometryHandler : NewGeometryFromChangesFeatureSource;
|
||||
|
||||
private readonly newGeometryHandler: NewGeometryFromChangesFeatureSource
|
||||
|
||||
constructor(
|
||||
handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void,
|
||||
|
@ -77,33 +76,40 @@ export default class FeaturePipeline {
|
|||
handleRawFeatureSource: (source: FeatureSourceForLayer) => void
|
||||
}
|
||||
) {
|
||||
this.state = state;
|
||||
this.state = state
|
||||
|
||||
const self = this
|
||||
const expiryInSeconds = Math.min(...state.layoutToUse?.layers?.map(l => l.maxAgeOfCache) ?? [])
|
||||
this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds);
|
||||
this.osmSourceZoomLevel = state.osmApiTileSize.data;
|
||||
const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12))
|
||||
const expiryInSeconds = Math.min(
|
||||
...(state.layoutToUse?.layers?.map((l) => l.maxAgeOfCache) ?? [])
|
||||
)
|
||||
this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds)
|
||||
this.osmSourceZoomLevel = state.osmApiTileSize.data
|
||||
const useOsmApi = state.locationControl.map(
|
||||
(l) => l.zoom > (state.overpassMaxZoom.data ?? 12)
|
||||
)
|
||||
this.relationTracker = new RelationsTracker()
|
||||
|
||||
state.changes.allChanges.addCallbackAndRun(allChanges => {
|
||||
allChanges.filter(ch => ch.id < 0 && ch.changes !== undefined)
|
||||
.map(ch => ch.changes)
|
||||
.filter(coor => coor["lat"] !== undefined && coor["lon"] !== undefined)
|
||||
.forEach(coor => {
|
||||
state.layoutToUse.layers.forEach(l => self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"]))
|
||||
state.changes.allChanges.addCallbackAndRun((allChanges) => {
|
||||
allChanges
|
||||
.filter((ch) => ch.id < 0 && ch.changes !== undefined)
|
||||
.map((ch) => ch.changes)
|
||||
.filter((coor) => coor["lat"] !== undefined && coor["lon"] !== undefined)
|
||||
.forEach((coor) => {
|
||||
state.layoutToUse.layers.forEach((l) =>
|
||||
self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
this.sufficientlyZoomed = state.locationControl.map(location => {
|
||||
if (location?.zoom === undefined) {
|
||||
return false;
|
||||
}
|
||||
let minzoom = Math.min(...state.filteredLayers.data.map(layer => layer.layerDef.minzoom ?? 18));
|
||||
return location.zoom >= minzoom;
|
||||
this.sufficientlyZoomed = state.locationControl.map((location) => {
|
||||
if (location?.zoom === undefined) {
|
||||
return false
|
||||
}
|
||||
);
|
||||
let minzoom = Math.min(
|
||||
...state.filteredLayers.data.map((layer) => layer.layerDef.minzoom ?? 18)
|
||||
)
|
||||
return location.zoom >= minzoom
|
||||
})
|
||||
|
||||
const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed)
|
||||
|
||||
|
@ -111,9 +117,11 @@ export default class FeaturePipeline {
|
|||
this.perLayerHierarchy = perLayerHierarchy
|
||||
|
||||
// Given a tile, wraps it and passes it on to render (handled by 'handleFeatureSource'
|
||||
function patchedHandleFeatureSource(src: FeatureSourceForLayer & IndexedFeatureSource & Tiled) {
|
||||
function patchedHandleFeatureSource(
|
||||
src: FeatureSourceForLayer & IndexedFeatureSource & Tiled
|
||||
) {
|
||||
// This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
|
||||
const withChanges = new ChangeGeometryApplicator(src, state.changes);
|
||||
const withChanges = new ChangeGeometryApplicator(src, state.changes)
|
||||
const srcFiltered = new FilteringFeatureSource(state, src.tileIndex, withChanges)
|
||||
|
||||
handleFeatureSource(srcFiltered)
|
||||
|
@ -127,31 +135,29 @@ export default class FeaturePipeline {
|
|||
function handlePriviligedFeatureSource(src: FeatureSourceForLayer & Tiled) {
|
||||
// Passthrough to passed function, except that it registers as well
|
||||
handleFeatureSource(src)
|
||||
src.features.addCallbackAndRunD(fs => {
|
||||
fs.forEach(ff => state.allElements.addOrGetElement(ff.feature))
|
||||
src.features.addCallbackAndRunD((fs) => {
|
||||
fs.forEach((ff) => state.allElements.addOrGetElement(ff.feature))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
for (const filteredLayer of state.filteredLayers.data) {
|
||||
const id = filteredLayer.layerDef.id
|
||||
const source = filteredLayer.layerDef.source
|
||||
|
||||
const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) => patchedHandleFeatureSource(tile))
|
||||
const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) =>
|
||||
patchedHandleFeatureSource(tile)
|
||||
)
|
||||
perLayerHierarchy.set(id, hierarchy)
|
||||
|
||||
this.freshnesses.set(id, new TileFreshnessCalculator())
|
||||
|
||||
if (id === "type_node") {
|
||||
|
||||
this.fullNodeDatabase = new FullNodeDatabaseSource(
|
||||
filteredLayer,
|
||||
tile => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
|
||||
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
|
||||
});
|
||||
continue;
|
||||
this.fullNodeDatabase = new FullNodeDatabaseSource(filteredLayer, (tile) => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
|
||||
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (id === "gps_location") {
|
||||
|
@ -187,13 +193,15 @@ export default class FeaturePipeline {
|
|||
// We load the cached values and register them
|
||||
// Getting data from upstream happens a bit lower
|
||||
localTileSaver.LoadTilesFromDisk(
|
||||
state.currentBounds, state.locationControl,
|
||||
(tileIndex, freshness) => self.freshnesses.get(id).addTileLoad(tileIndex, freshness),
|
||||
state.currentBounds,
|
||||
state.locationControl,
|
||||
(tileIndex, freshness) =>
|
||||
self.freshnesses.get(id).addTileLoad(tileIndex, freshness),
|
||||
(tile) => {
|
||||
console.debug("Loaded tile ", id, tile.tileIndex, "from local cache")
|
||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||
hierarchy.registerTile(tile);
|
||||
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
|
||||
hierarchy.registerTile(tile)
|
||||
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -213,47 +221,48 @@ export default class FeaturePipeline {
|
|||
registerTile: (tile) => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||
perLayerHierarchy.get(id).registerTile(tile)
|
||||
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
|
||||
}
|
||||
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
|
||||
},
|
||||
})
|
||||
} else {
|
||||
new RegisteringAllFromFeatureSourceActor(src, state.allElements)
|
||||
perLayerHierarchy.get(id).registerTile(src)
|
||||
src.features.addCallbackAndRunD(_ => self.onNewDataLoaded(src))
|
||||
src.features.addCallbackAndRunD((_) => self.onNewDataLoaded(src))
|
||||
}
|
||||
} else {
|
||||
new DynamicGeoJsonTileSource(
|
||||
filteredLayer,
|
||||
tile => {
|
||||
(tile) => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||
perLayerHierarchy.get(id).registerTile(tile)
|
||||
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
|
||||
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
|
||||
},
|
||||
state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const osmFeatureSource = new OsmFeatureSource({
|
||||
isActive: useOsmApi,
|
||||
neededTiles: neededTilesFromOsm,
|
||||
handleTile: tile => {
|
||||
handleTile: (tile) => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||
if (tile.layer.layerDef.maxAgeOfCache > 0) {
|
||||
const saver = self.localStorageSavers.get(tile.layer.layerDef.id)
|
||||
if (saver === undefined) {
|
||||
console.error("No localStorageSaver found for layer ", tile.layer.layerDef.id)
|
||||
console.error(
|
||||
"No localStorageSaver found for layer ",
|
||||
tile.layer.layerDef.id
|
||||
)
|
||||
}
|
||||
saver?.addTile(tile)
|
||||
}
|
||||
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
|
||||
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
|
||||
|
||||
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
|
||||
},
|
||||
state: state,
|
||||
markTileVisited: (tileId) =>
|
||||
state.filteredLayers.data.forEach(flayer => {
|
||||
state.filteredLayers.data.forEach((flayer) => {
|
||||
const layer = flayer.layerDef
|
||||
if (layer.maxAgeOfCache > 0) {
|
||||
const saver = self.localStorageSavers.get(layer.id)
|
||||
|
@ -264,110 +273,128 @@ export default class FeaturePipeline {
|
|||
}
|
||||
}
|
||||
self.freshnesses.get(layer.id).addTileLoad(tileId, new Date())
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
if (this.fullNodeDatabase !== undefined) {
|
||||
osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => this.fullNodeDatabase.handleOsmJson(osmJson, tileId))
|
||||
osmFeatureSource.rawDataHandlers.push((osmJson, tileId) =>
|
||||
this.fullNodeDatabase.handleOsmJson(osmJson, tileId)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const updater = this.initOverpassUpdater(state, useOsmApi)
|
||||
this.overpassUpdater = updater;
|
||||
this.overpassUpdater = updater
|
||||
this.timeout = updater.timeout
|
||||
|
||||
// Actually load data from the overpass source
|
||||
new PerLayerFeatureSourceSplitter(state.filteredLayers,
|
||||
(source) => TiledFeatureSource.createHierarchy(source, {
|
||||
layer: source.layer,
|
||||
minZoomLevel: source.layer.layerDef.minzoom,
|
||||
noDuplicates: true,
|
||||
maxFeatureCount: state.layoutToUse.clustering.minNeededElements,
|
||||
maxZoomLevel: state.layoutToUse.clustering.maxZoom,
|
||||
registerTile: (tile) => {
|
||||
// We save the tile data for the given layer to local storage - data sourced from overpass
|
||||
self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile)
|
||||
perLayerHierarchy.get(source.layer.layerDef.id).registerTile(new RememberingSource(tile))
|
||||
tile.features.addCallbackAndRunD(f => {
|
||||
if (f.length === 0) {
|
||||
return
|
||||
}
|
||||
self.onNewDataLoaded(tile)
|
||||
})
|
||||
|
||||
}
|
||||
}),
|
||||
new PerLayerFeatureSourceSplitter(
|
||||
state.filteredLayers,
|
||||
(source) =>
|
||||
TiledFeatureSource.createHierarchy(source, {
|
||||
layer: source.layer,
|
||||
minZoomLevel: source.layer.layerDef.minzoom,
|
||||
noDuplicates: true,
|
||||
maxFeatureCount: state.layoutToUse.clustering.minNeededElements,
|
||||
maxZoomLevel: state.layoutToUse.clustering.maxZoom,
|
||||
registerTile: (tile) => {
|
||||
// We save the tile data for the given layer to local storage - data sourced from overpass
|
||||
self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile)
|
||||
perLayerHierarchy
|
||||
.get(source.layer.layerDef.id)
|
||||
.registerTile(new RememberingSource(tile))
|
||||
tile.features.addCallbackAndRunD((f) => {
|
||||
if (f.length === 0) {
|
||||
return
|
||||
}
|
||||
self.onNewDataLoaded(tile)
|
||||
})
|
||||
},
|
||||
}),
|
||||
updater,
|
||||
{
|
||||
handleLeftovers: (leftOvers) => {
|
||||
console.warn("Overpass returned a few non-matched features:", leftOvers)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
// Also load points/lines that are newly added.
|
||||
const newGeometry = new NewGeometryFromChangesFeatureSource(state.changes, state.allElements, state.osmConnection._oauth_config.url)
|
||||
this.newGeometryHandler = newGeometry;
|
||||
newGeometry.features.addCallbackAndRun(geometries => {
|
||||
// Also load points/lines that are newly added.
|
||||
const newGeometry = new NewGeometryFromChangesFeatureSource(
|
||||
state.changes,
|
||||
state.allElements,
|
||||
state.osmConnection._oauth_config.url
|
||||
)
|
||||
this.newGeometryHandler = newGeometry
|
||||
newGeometry.features.addCallbackAndRun((geometries) => {
|
||||
console.debug("New geometries are:", geometries)
|
||||
})
|
||||
|
||||
new RegisteringAllFromFeatureSourceActor(newGeometry, state.allElements)
|
||||
// A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next
|
||||
new PerLayerFeatureSourceSplitter(state.filteredLayers,
|
||||
new PerLayerFeatureSourceSplitter(
|
||||
state.filteredLayers,
|
||||
(perLayer) => {
|
||||
// We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
|
||||
perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer)
|
||||
// AT last, we always apply the metatags whenever possible
|
||||
perLayer.features.addCallbackAndRunD(_ => {
|
||||
self.onNewDataLoaded(perLayer);
|
||||
perLayer.features.addCallbackAndRunD((_) => {
|
||||
self.onNewDataLoaded(perLayer)
|
||||
})
|
||||
|
||||
},
|
||||
newGeometry,
|
||||
{
|
||||
handleLeftovers: (leftOvers) => {
|
||||
console.warn("Got some leftovers from the filteredLayers: ", leftOvers)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
this.runningQuery = updater.runningQuery.map(
|
||||
overpass => {
|
||||
console.log("FeaturePipeline: runningQuery state changed: Overpass", overpass ? "is querying," : "is idle,",
|
||||
"osmFeatureSource is", osmFeatureSource.isRunning ? "is running and needs " + neededTilesFromOsm.data?.length + " tiles (already got " + osmFeatureSource.downloadedTiles.size + " tiles )" : "is idle")
|
||||
return overpass || osmFeatureSource.isRunning.data;
|
||||
}, [osmFeatureSource.isRunning]
|
||||
(overpass) => {
|
||||
console.log(
|
||||
"FeaturePipeline: runningQuery state changed: Overpass",
|
||||
overpass ? "is querying," : "is idle,",
|
||||
"osmFeatureSource is",
|
||||
osmFeatureSource.isRunning
|
||||
? "is running and needs " +
|
||||
neededTilesFromOsm.data?.length +
|
||||
" tiles (already got " +
|
||||
osmFeatureSource.downloadedTiles.size +
|
||||
" tiles )"
|
||||
: "is idle"
|
||||
)
|
||||
return overpass || osmFeatureSource.isRunning.data
|
||||
},
|
||||
[osmFeatureSource.isRunning]
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
public GetAllFeaturesWithin(bbox: BBox): OsmFeature[][] {
|
||||
const self = this
|
||||
const tiles: OsmFeature[][] = []
|
||||
Array.from(this.perLayerHierarchy.keys())
|
||||
.forEach(key => {
|
||||
const fetched : OsmFeature[][] = self.GetFeaturesWithin(key, bbox)
|
||||
tiles.push(...fetched);
|
||||
})
|
||||
return tiles;
|
||||
Array.from(this.perLayerHierarchy.keys()).forEach((key) => {
|
||||
const fetched: OsmFeature[][] = self.GetFeaturesWithin(key, bbox)
|
||||
tiles.push(...fetched)
|
||||
})
|
||||
return tiles
|
||||
}
|
||||
|
||||
public GetAllFeaturesAndMetaWithin(bbox: BBox, layerIdWhitelist?: Set<string>):
|
||||
{features: OsmFeature[], layer: string}[] {
|
||||
public GetAllFeaturesAndMetaWithin(
|
||||
bbox: BBox,
|
||||
layerIdWhitelist?: Set<string>
|
||||
): { features: OsmFeature[]; layer: string }[] {
|
||||
const self = this
|
||||
const tiles :{features: any[], layer: string}[]= []
|
||||
Array.from(this.perLayerHierarchy.keys())
|
||||
.forEach(key => {
|
||||
if(layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)){
|
||||
return;
|
||||
}
|
||||
return tiles.push({
|
||||
layer: key,
|
||||
features: [].concat(...self.GetFeaturesWithin(key, bbox))
|
||||
});
|
||||
const tiles: { features: any[]; layer: string }[] = []
|
||||
Array.from(this.perLayerHierarchy.keys()).forEach((key) => {
|
||||
if (layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)) {
|
||||
return
|
||||
}
|
||||
return tiles.push({
|
||||
layer: key,
|
||||
features: [].concat(...self.GetFeaturesWithin(key, bbox)),
|
||||
})
|
||||
return tiles;
|
||||
})
|
||||
return tiles
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -380,16 +407,24 @@ export default class FeaturePipeline {
|
|||
}
|
||||
const requestedHierarchy = this.perLayerHierarchy.get(layerId)
|
||||
if (requestedHierarchy === undefined) {
|
||||
console.warn("Layer ", layerId, "is not defined. Try one of ", Array.from(this.perLayerHierarchy.keys()))
|
||||
return undefined;
|
||||
console.warn(
|
||||
"Layer ",
|
||||
layerId,
|
||||
"is not defined. Try one of ",
|
||||
Array.from(this.perLayerHierarchy.keys())
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
return TileHierarchyTools.getTiles(requestedHierarchy, bbox)
|
||||
.filter(featureSource => featureSource.features?.data !== undefined)
|
||||
.map(featureSource => featureSource.features.data.map(fs => fs.feature))
|
||||
.filter((featureSource) => featureSource.features?.data !== undefined)
|
||||
.map((featureSource) => featureSource.features.data.map((fs) => fs.feature))
|
||||
}
|
||||
|
||||
public GetTilesPerLayerWithin(bbox: BBox, handleTile: (tile: FeatureSourceForLayer & Tiled) => void) {
|
||||
Array.from(this.perLayerHierarchy.values()).forEach(hierarchy => {
|
||||
public GetTilesPerLayerWithin(
|
||||
bbox: BBox,
|
||||
handleTile: (tile: FeatureSourceForLayer & Tiled) => void
|
||||
) {
|
||||
Array.from(this.perLayerHierarchy.values()).forEach((hierarchy) => {
|
||||
TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile)
|
||||
})
|
||||
}
|
||||
|
@ -399,16 +434,16 @@ export default class FeaturePipeline {
|
|||
}
|
||||
|
||||
private freshnessForVisibleLayers(z: number, x: number, y: number): Date {
|
||||
let oldestDate = undefined;
|
||||
let oldestDate = undefined
|
||||
for (const flayer of this.state.filteredLayers.data) {
|
||||
if (!flayer.isDisplayed.data && !flayer.layerDef.forceLoad) {
|
||||
continue
|
||||
}
|
||||
if (this.state.locationControl.data.zoom < flayer.layerDef.minzoom) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (flayer.layerDef.maxAgeOfCache === 0) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
const freshnessCalc = this.freshnesses.get(flayer.layerDef.id)
|
||||
if (freshnessCalc === undefined) {
|
||||
|
@ -428,117 +463,136 @@ export default class FeaturePipeline {
|
|||
}
|
||||
|
||||
/*
|
||||
* Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM
|
||||
* */
|
||||
* Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM
|
||||
* */
|
||||
private getNeededTilesFromOsm(isSufficientlyZoomed: Store<boolean>): Store<number[]> {
|
||||
const self = this
|
||||
return this.state.currentBounds.map(bbox => {
|
||||
if (bbox === undefined) {
|
||||
return []
|
||||
}
|
||||
if (!isSufficientlyZoomed.data) {
|
||||
return [];
|
||||
}
|
||||
const osmSourceZoomLevel = self.osmSourceZoomLevel
|
||||
const range = bbox.containingTileRange(osmSourceZoomLevel)
|
||||
const tileIndexes = []
|
||||
if (range.total >= 100) {
|
||||
// Too much tiles!
|
||||
return undefined
|
||||
}
|
||||
Tiles.MapRange(range, (x, y) => {
|
||||
const i = Tiles.tile_index(osmSourceZoomLevel, x, y);
|
||||
const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y)
|
||||
if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) {
|
||||
console.debug("Skipping tile", osmSourceZoomLevel, x, y, "as a decently fresh one is available")
|
||||
// The cached tiles contain decently fresh data
|
||||
return undefined;
|
||||
return this.state.currentBounds.map(
|
||||
(bbox) => {
|
||||
if (bbox === undefined) {
|
||||
return []
|
||||
}
|
||||
tileIndexes.push(i)
|
||||
})
|
||||
return tileIndexes
|
||||
}, [isSufficientlyZoomed])
|
||||
if (!isSufficientlyZoomed.data) {
|
||||
return []
|
||||
}
|
||||
const osmSourceZoomLevel = self.osmSourceZoomLevel
|
||||
const range = bbox.containingTileRange(osmSourceZoomLevel)
|
||||
const tileIndexes = []
|
||||
if (range.total >= 100) {
|
||||
// Too much tiles!
|
||||
return undefined
|
||||
}
|
||||
Tiles.MapRange(range, (x, y) => {
|
||||
const i = Tiles.tile_index(osmSourceZoomLevel, x, y)
|
||||
const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y)
|
||||
if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) {
|
||||
console.debug(
|
||||
"Skipping tile",
|
||||
osmSourceZoomLevel,
|
||||
x,
|
||||
y,
|
||||
"as a decently fresh one is available"
|
||||
)
|
||||
// The cached tiles contain decently fresh data
|
||||
return undefined
|
||||
}
|
||||
tileIndexes.push(i)
|
||||
})
|
||||
return tileIndexes
|
||||
},
|
||||
[isSufficientlyZoomed]
|
||||
)
|
||||
}
|
||||
|
||||
private initOverpassUpdater(state: {
|
||||
allElements: ElementStorage;
|
||||
layoutToUse: LayoutConfig,
|
||||
currentBounds: Store<BBox>,
|
||||
locationControl: Store<Loc>,
|
||||
readonly overpassUrl: Store<string[]>;
|
||||
readonly overpassTimeout: Store<number>;
|
||||
readonly overpassMaxZoom: Store<number>,
|
||||
}, useOsmApi: Store<boolean>): OverpassFeatureSource {
|
||||
const minzoom = Math.min(...state.layoutToUse.layers.map(layer => layer.minzoom))
|
||||
const overpassIsActive = state.currentBounds.map(bbox => {
|
||||
if (bbox === undefined) {
|
||||
console.debug("Disabling overpass source: no bbox")
|
||||
return false
|
||||
}
|
||||
let zoom = state.locationControl.data.zoom
|
||||
if (zoom < minzoom) {
|
||||
// We are zoomed out over the zoomlevel of any layer
|
||||
console.debug("Disabling overpass source: zoom < minzoom")
|
||||
return false;
|
||||
}
|
||||
|
||||
const range = bbox.containingTileRange(zoom)
|
||||
if (range.total >= 5000) {
|
||||
// Let's assume we don't have so much data cached
|
||||
return true
|
||||
}
|
||||
const self = this;
|
||||
const allFreshnesses = Tiles.MapRange(range, (x, y) => self.freshnessForVisibleLayers(zoom, x, y))
|
||||
return allFreshnesses.some(freshness => freshness === undefined || freshness < this.oldestAllowedDate)
|
||||
}, [state.locationControl])
|
||||
|
||||
const self = this;
|
||||
const updater = new OverpassFeatureSource(state,
|
||||
{
|
||||
padToTiles: state.locationControl.map(l => Math.min(15, l.zoom + 1)),
|
||||
relationTracker: this.relationTracker,
|
||||
isActive: useOsmApi.map(b => !b && overpassIsActive.data, [overpassIsActive]),
|
||||
freshnesses: this.freshnesses,
|
||||
onBboxLoaded: (bbox, date, downloadedLayers, paddedToZoomLevel) => {
|
||||
Tiles.MapRange(bbox.containingTileRange(paddedToZoomLevel), (x, y) => {
|
||||
const tileIndex = Tiles.tile_index(paddedToZoomLevel, x, y)
|
||||
downloadedLayers.forEach(layer => {
|
||||
self.freshnesses.get(layer.id).addTileLoad(tileIndex, date)
|
||||
self.localStorageSavers.get(layer.id)?.MarkVisited(tileIndex, date)
|
||||
})
|
||||
})
|
||||
|
||||
private initOverpassUpdater(
|
||||
state: {
|
||||
allElements: ElementStorage
|
||||
layoutToUse: LayoutConfig
|
||||
currentBounds: Store<BBox>
|
||||
locationControl: Store<Loc>
|
||||
readonly overpassUrl: Store<string[]>
|
||||
readonly overpassTimeout: Store<number>
|
||||
readonly overpassMaxZoom: Store<number>
|
||||
},
|
||||
useOsmApi: Store<boolean>
|
||||
): OverpassFeatureSource {
|
||||
const minzoom = Math.min(...state.layoutToUse.layers.map((layer) => layer.minzoom))
|
||||
const overpassIsActive = state.currentBounds.map(
|
||||
(bbox) => {
|
||||
if (bbox === undefined) {
|
||||
console.debug("Disabling overpass source: no bbox")
|
||||
return false
|
||||
}
|
||||
let zoom = state.locationControl.data.zoom
|
||||
if (zoom < minzoom) {
|
||||
// We are zoomed out over the zoomlevel of any layer
|
||||
console.debug("Disabling overpass source: zoom < minzoom")
|
||||
return false
|
||||
}
|
||||
});
|
||||
|
||||
const range = bbox.containingTileRange(zoom)
|
||||
if (range.total >= 5000) {
|
||||
// Let's assume we don't have so much data cached
|
||||
return true
|
||||
}
|
||||
const self = this
|
||||
const allFreshnesses = Tiles.MapRange(range, (x, y) =>
|
||||
self.freshnessForVisibleLayers(zoom, x, y)
|
||||
)
|
||||
return allFreshnesses.some(
|
||||
(freshness) => freshness === undefined || freshness < this.oldestAllowedDate
|
||||
)
|
||||
},
|
||||
[state.locationControl]
|
||||
)
|
||||
|
||||
const self = this
|
||||
const updater = new OverpassFeatureSource(state, {
|
||||
padToTiles: state.locationControl.map((l) => Math.min(15, l.zoom + 1)),
|
||||
relationTracker: this.relationTracker,
|
||||
isActive: useOsmApi.map((b) => !b && overpassIsActive.data, [overpassIsActive]),
|
||||
freshnesses: this.freshnesses,
|
||||
onBboxLoaded: (bbox, date, downloadedLayers, paddedToZoomLevel) => {
|
||||
Tiles.MapRange(bbox.containingTileRange(paddedToZoomLevel), (x, y) => {
|
||||
const tileIndex = Tiles.tile_index(paddedToZoomLevel, x, y)
|
||||
downloadedLayers.forEach((layer) => {
|
||||
self.freshnesses.get(layer.id).addTileLoad(tileIndex, date)
|
||||
self.localStorageSavers.get(layer.id)?.MarkVisited(tileIndex, date)
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// Register everything in the state' 'AllElements'
|
||||
new RegisteringAllFromFeatureSourceActor(updater, state.allElements)
|
||||
return updater;
|
||||
return updater
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds upon 'GetAllFeaturesAndMetaWithin', but does stricter BBOX-checking and applies the filters
|
||||
*/
|
||||
public getAllVisibleElementsWithmeta(bbox: BBox): { center: [number, number], element: OsmFeature, layer: LayerConfig }[] {
|
||||
public getAllVisibleElementsWithmeta(
|
||||
bbox: BBox
|
||||
): { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] {
|
||||
if (bbox === undefined) {
|
||||
console.warn("No bbox")
|
||||
return []
|
||||
}
|
||||
|
||||
const layers = Utils.toIdRecord(this.state.layoutToUse.layers)
|
||||
const elementsWithMeta: { features: OsmFeature[], layer: string }[] = this.GetAllFeaturesAndMetaWithin(bbox)
|
||||
const elementsWithMeta: { features: OsmFeature[]; layer: string }[] =
|
||||
this.GetAllFeaturesAndMetaWithin(bbox)
|
||||
|
||||
let elements: {center: [number, number], element: OsmFeature, layer: LayerConfig }[] = []
|
||||
let elements: { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] = []
|
||||
let seenElements = new Set<string>()
|
||||
for (const elementsWithMetaElement of elementsWithMeta) {
|
||||
const layer = layers[elementsWithMetaElement.layer]
|
||||
if(layer.title === undefined){
|
||||
if (layer.title === undefined) {
|
||||
continue
|
||||
}
|
||||
const filtered = this.state.filteredLayers.data.find(fl => fl.layerDef == layer);
|
||||
const filtered = this.state.filteredLayers.data.find((fl) => fl.layerDef == layer)
|
||||
for (let i = 0; i < elementsWithMetaElement.features.length; i++) {
|
||||
const element = elementsWithMetaElement.features[i];
|
||||
const element = elementsWithMetaElement.features[i]
|
||||
if (!filtered.isDisplayed.data) {
|
||||
continue
|
||||
}
|
||||
|
@ -552,35 +606,38 @@ export default class FeaturePipeline {
|
|||
if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) {
|
||||
continue
|
||||
}
|
||||
const activeFilters: FilterState[] = Array.from(filtered.appliedFilters.data.values());
|
||||
if (!activeFilters.every(filter => filter?.currentFilter === undefined || filter?.currentFilter?.matchesProperties(element.properties))) {
|
||||
const activeFilters: FilterState[] = Array.from(
|
||||
filtered.appliedFilters.data.values()
|
||||
)
|
||||
if (
|
||||
!activeFilters.every(
|
||||
(filter) =>
|
||||
filter?.currentFilter === undefined ||
|
||||
filter?.currentFilter?.matchesProperties(element.properties)
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const center = GeoOperations.centerpointCoordinates(element);
|
||||
const center = GeoOperations.centerpointCoordinates(element)
|
||||
elements.push({
|
||||
element,
|
||||
center,
|
||||
layer: layers[elementsWithMetaElement.layer],
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return elements;
|
||||
return elements
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inject a new point
|
||||
* Inject a new point
|
||||
*/
|
||||
InjectNewPoint(geojson) {
|
||||
this.newGeometryHandler.features.data.push({
|
||||
feature: geojson,
|
||||
freshness: new Date()
|
||||
freshness: new Date(),
|
||||
})
|
||||
this.newGeometryHandler.features.ping();
|
||||
this.newGeometryHandler.features.ping()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import {Store, UIEventSource} from "../UIEventSource";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import {BBox} from "../BBox";
|
||||
import {Feature, Geometry} from "@turf/turf";
|
||||
import {OsmFeature} from "../../Models/OsmFeature";
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import { BBox } from "../BBox"
|
||||
import { Feature, Geometry } from "@turf/turf"
|
||||
import { OsmFeature } from "../../Models/OsmFeature"
|
||||
|
||||
export default interface FeatureSource {
|
||||
features: Store<{ feature: OsmFeature, freshness: Date }[]>;
|
||||
features: Store<{ feature: OsmFeature; freshness: Date }[]>
|
||||
/**
|
||||
* Mainly used for debuging
|
||||
*/
|
||||
name: string;
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface Tiled {
|
||||
tileIndex: number,
|
||||
tileIndex: number
|
||||
bbox: BBox
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource";
|
||||
import {Store} from "../UIEventSource";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
|
||||
|
||||
import FeatureSource, { FeatureSourceForLayer, Tiled } from "./FeatureSource"
|
||||
import { Store } from "../UIEventSource"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import SimpleFeatureSource from "./Sources/SimpleFeatureSource"
|
||||
|
||||
/**
|
||||
* In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled)
|
||||
|
@ -10,30 +9,30 @@ import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
|
|||
* In any case, this featureSource marks the objects with _matching_layer_id
|
||||
*/
|
||||
export default class PerLayerFeatureSourceSplitter {
|
||||
|
||||
constructor(layers: Store<FilteredLayer[]>,
|
||||
handleLayerData: (source: FeatureSourceForLayer & Tiled) => void,
|
||||
upstream: FeatureSource,
|
||||
options?: {
|
||||
tileIndex?: number,
|
||||
handleLeftovers?: (featuresWithoutLayer: any[]) => void
|
||||
}) {
|
||||
|
||||
constructor(
|
||||
layers: Store<FilteredLayer[]>,
|
||||
handleLayerData: (source: FeatureSourceForLayer & Tiled) => void,
|
||||
upstream: FeatureSource,
|
||||
options?: {
|
||||
tileIndex?: number
|
||||
handleLeftovers?: (featuresWithoutLayer: any[]) => void
|
||||
}
|
||||
) {
|
||||
const knownLayers = new Map<string, SimpleFeatureSource>()
|
||||
|
||||
function update() {
|
||||
const features = upstream.features?.data;
|
||||
const features = upstream.features?.data
|
||||
if (features === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (layers.data === undefined || layers.data.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// We try to figure out (for each feature) in which feature store it should be saved.
|
||||
// Note that this splitter is only run when it is invoked by the overpass feature source, so we can't be sure in which layer it should go
|
||||
|
||||
const featuresPerLayer = new Map<string, { feature, freshness } []>();
|
||||
const featuresPerLayer = new Map<string, { feature; freshness }[]>()
|
||||
const noLayerFound = []
|
||||
|
||||
for (const layer of layers.data) {
|
||||
|
@ -41,19 +40,19 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
}
|
||||
|
||||
for (const f of features) {
|
||||
let foundALayer = false;
|
||||
let foundALayer = false
|
||||
for (const layer of layers.data) {
|
||||
if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) {
|
||||
// We have found our matching layer!
|
||||
featuresPerLayer.get(layer.layerDef.id).push(f)
|
||||
foundALayer = true;
|
||||
foundALayer = true
|
||||
if (!layer.layerDef.passAllFeatures) {
|
||||
// If not 'passAllFeatures', we are done for this feature
|
||||
break
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!foundALayer){
|
||||
if (!foundALayer) {
|
||||
noLayerFound.push(f)
|
||||
}
|
||||
}
|
||||
|
@ -61,11 +60,11 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
// At this point, we have our features per layer as a list
|
||||
// We assign them to the correct featureSources
|
||||
for (const layer of layers.data) {
|
||||
const id = layer.layerDef.id;
|
||||
const id = layer.layerDef.id
|
||||
const features = featuresPerLayer.get(id)
|
||||
if (features === undefined) {
|
||||
// No such features for this layer
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
let featureSource = knownLayers.get(id)
|
||||
|
@ -86,7 +85,7 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
}
|
||||
}
|
||||
|
||||
layers.addCallback(_ => update())
|
||||
upstream.features.addCallbackAndRunD(_ => update())
|
||||
layers.addCallback((_) => update())
|
||||
upstream.features.addCallbackAndRunD((_) => update())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +1,52 @@
|
|||
/**
|
||||
* Applies geometry changes from 'Changes' onto every feature of a featureSource
|
||||
*/
|
||||
import {Changes} from "../../Osm/Changes";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import {FeatureSourceForLayer, IndexedFeatureSource} from "../FeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {ChangeDescription, ChangeDescriptionTools} from "../../Osm/Actions/ChangeDescription";
|
||||
|
||||
import { Changes } from "../../Osm/Changes"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import { FeatureSourceForLayer, IndexedFeatureSource } from "../FeatureSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription"
|
||||
|
||||
export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string;
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> =
|
||||
new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
public readonly name: string
|
||||
public readonly layer: FilteredLayer
|
||||
private readonly source: IndexedFeatureSource;
|
||||
private readonly changes: Changes;
|
||||
private readonly source: IndexedFeatureSource
|
||||
private readonly changes: Changes
|
||||
|
||||
constructor(source: (IndexedFeatureSource & FeatureSourceForLayer), changes: Changes) {
|
||||
this.source = source;
|
||||
this.changes = changes;
|
||||
constructor(source: IndexedFeatureSource & FeatureSourceForLayer, changes: Changes) {
|
||||
this.source = source
|
||||
this.changes = changes
|
||||
this.layer = source.layer
|
||||
|
||||
this.name = "ChangesApplied(" + source.name + ")"
|
||||
this.features = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined)
|
||||
|
||||
const self = this;
|
||||
source.features.addCallbackAndRunD(_ => self.update())
|
||||
|
||||
changes.allChanges.addCallbackAndRunD(_ => self.update())
|
||||
const self = this
|
||||
source.features.addCallbackAndRunD((_) => self.update())
|
||||
|
||||
changes.allChanges.addCallbackAndRunD((_) => self.update())
|
||||
}
|
||||
|
||||
private update() {
|
||||
const upstreamFeatures = this.source.features.data
|
||||
const upstreamIds = this.source.containedIds.data
|
||||
const changesToApply = this.changes.allChanges.data
|
||||
?.filter(ch =>
|
||||
const changesToApply = this.changes.allChanges.data?.filter(
|
||||
(ch) =>
|
||||
// Does upsteram have this element? If not, we skip
|
||||
upstreamIds.has(ch.type + "/" + ch.id) &&
|
||||
// Are any (geometry) changes defined?
|
||||
ch.changes !== undefined &&
|
||||
// Ignore new elements, they are handled by the NewGeometryFromChangesFeatureSource
|
||||
ch.id > 0)
|
||||
ch.id > 0
|
||||
)
|
||||
|
||||
if (changesToApply === undefined || changesToApply.length === 0) {
|
||||
// No changes to apply!
|
||||
// Pass the original feature and lets continue our day
|
||||
this.features.setData(upstreamFeatures);
|
||||
return;
|
||||
this.features.setData(upstreamFeatures)
|
||||
return
|
||||
}
|
||||
|
||||
const changesPerId = new Map<string, ChangeDescription[]>()
|
||||
|
@ -58,27 +58,32 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
|||
changesPerId.set(key, [ch])
|
||||
}
|
||||
}
|
||||
const newFeatures: { feature: any, freshness: Date }[] = []
|
||||
const newFeatures: { feature: any; freshness: Date }[] = []
|
||||
for (const feature of upstreamFeatures) {
|
||||
const changesForFeature = changesPerId.get(feature.feature.properties.id)
|
||||
if (changesForFeature === undefined) {
|
||||
// No changes for this element
|
||||
newFeatures.push(feature)
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
// Allright! We have a feature to rewrite!
|
||||
const copy = {
|
||||
...feature
|
||||
...feature,
|
||||
}
|
||||
// We only apply the last change as that one'll have the latest geometry
|
||||
const change = changesForFeature[changesForFeature.length - 1]
|
||||
copy.feature.geometry = ChangeDescriptionTools.getGeojsonGeometry(change)
|
||||
console.log("Applying a geometry change onto:", feature,"The change is:", change,"which becomes:", copy)
|
||||
console.log(
|
||||
"Applying a geometry change onto:",
|
||||
feature,
|
||||
"The change is:",
|
||||
change,
|
||||
"which becomes:",
|
||||
copy
|
||||
)
|
||||
newFeatures.push(copy)
|
||||
}
|
||||
this.features.setData(newFeatures)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,99 +1,112 @@
|
|||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
|
||||
export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource {
|
||||
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name;
|
||||
export default class FeatureSourceMerger
|
||||
implements FeatureSourceForLayer, Tiled, IndexedFeatureSource
|
||||
{
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<
|
||||
{ feature: any; freshness: Date }[]
|
||||
>([])
|
||||
public readonly name
|
||||
public readonly layer: FilteredLayer
|
||||
public readonly tileIndex: number;
|
||||
public readonly bbox: BBox;
|
||||
public readonly containedIds: UIEventSource<Set<string>> = new UIEventSource<Set<string>>(new Set())
|
||||
private readonly _sources: UIEventSource<FeatureSource[]>;
|
||||
public readonly tileIndex: number
|
||||
public readonly bbox: BBox
|
||||
public readonly containedIds: UIEventSource<Set<string>> = new UIEventSource<Set<string>>(
|
||||
new Set()
|
||||
)
|
||||
private readonly _sources: UIEventSource<FeatureSource[]>
|
||||
/**
|
||||
* Merges features from different featureSources for a single layer
|
||||
* Uses the freshest feature available in the case multiple sources offer data with the same identifier
|
||||
*/
|
||||
constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource<FeatureSource[]>) {
|
||||
this.tileIndex = tileIndex;
|
||||
this.bbox = bbox;
|
||||
this._sources = sources;
|
||||
this.layer = layer;
|
||||
this.name = "FeatureSourceMerger(" + layer.layerDef.id + ", " + Tiles.tile_from_index(tileIndex).join(",") + ")"
|
||||
const self = this;
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
tileIndex: number,
|
||||
bbox: BBox,
|
||||
sources: UIEventSource<FeatureSource[]>
|
||||
) {
|
||||
this.tileIndex = tileIndex
|
||||
this.bbox = bbox
|
||||
this._sources = sources
|
||||
this.layer = layer
|
||||
this.name =
|
||||
"FeatureSourceMerger(" +
|
||||
layer.layerDef.id +
|
||||
", " +
|
||||
Tiles.tile_from_index(tileIndex).join(",") +
|
||||
")"
|
||||
const self = this
|
||||
|
||||
const handledSources = new Set<FeatureSource>();
|
||||
const handledSources = new Set<FeatureSource>()
|
||||
|
||||
sources.addCallbackAndRunD(sources => {
|
||||
let newSourceRegistered = false;
|
||||
sources.addCallbackAndRunD((sources) => {
|
||||
let newSourceRegistered = false
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
let source = sources[i];
|
||||
let source = sources[i]
|
||||
if (handledSources.has(source)) {
|
||||
continue
|
||||
}
|
||||
handledSources.add(source)
|
||||
newSourceRegistered = true
|
||||
source.features.addCallback(() => {
|
||||
self.Update();
|
||||
});
|
||||
self.Update()
|
||||
})
|
||||
if (newSourceRegistered) {
|
||||
self.Update();
|
||||
self.Update()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private Update() {
|
||||
|
||||
let somethingChanged = false;
|
||||
const all: Map<string, { feature: any, freshness: Date }> = new Map<string, { feature: any; freshness: Date }>();
|
||||
let somethingChanged = false
|
||||
const all: Map<string, { feature: any; freshness: Date }> = new Map<
|
||||
string,
|
||||
{ feature: any; freshness: Date }
|
||||
>()
|
||||
// We seed the dictionary with the previously loaded features
|
||||
const oldValues = this.features.data ?? [];
|
||||
const oldValues = this.features.data ?? []
|
||||
for (const oldValue of oldValues) {
|
||||
all.set(oldValue.feature.id, oldValue)
|
||||
}
|
||||
|
||||
for (const source of this._sources.data) {
|
||||
if (source?.features?.data === undefined) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
for (const f of source.features.data) {
|
||||
const id = f.feature.properties.id;
|
||||
const id = f.feature.properties.id
|
||||
if (!all.has(id)) {
|
||||
// This is a new feature
|
||||
somethingChanged = true;
|
||||
all.set(id, f);
|
||||
continue;
|
||||
somethingChanged = true
|
||||
all.set(id, f)
|
||||
continue
|
||||
}
|
||||
|
||||
// This value has been seen already, either in a previous run or by a previous datasource
|
||||
// Let's figure out if something changed
|
||||
const oldV = all.get(id);
|
||||
const oldV = all.get(id)
|
||||
if (oldV.freshness < f.freshness) {
|
||||
// Jup, this feature is fresher
|
||||
all.set(id, f);
|
||||
somethingChanged = true;
|
||||
all.set(id, f)
|
||||
somethingChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!somethingChanged) {
|
||||
// We don't bother triggering an update
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const newList = [];
|
||||
const newList = []
|
||||
all.forEach((value, _) => {
|
||||
newList.push(value)
|
||||
})
|
||||
this.containedIds.setData(new Set(all.keys()))
|
||||
this.features.setData(newList);
|
||||
this.features.setData(newList)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,35 @@
|
|||
import {Store, UIEventSource} from "../../UIEventSource";
|
||||
import FilteredLayer, {FilterState} from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {BBox} from "../../BBox";
|
||||
import {ElementStorage} from "../../ElementStorage";
|
||||
import {TagsFilter} from "../../Tags/TagsFilter";
|
||||
import {OsmFeature} from "../../../Models/OsmFeature";
|
||||
import { Store, UIEventSource } from "../../UIEventSource"
|
||||
import FilteredLayer, { FilterState } from "../../../Models/FilteredLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { BBox } from "../../BBox"
|
||||
import { ElementStorage } from "../../ElementStorage"
|
||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||
import { OsmFeature } from "../../../Models/OsmFeature"
|
||||
|
||||
export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled {
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> =
|
||||
new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name;
|
||||
public readonly layer: FilteredLayer;
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<
|
||||
{ feature: any; freshness: Date }[]
|
||||
>([])
|
||||
public readonly name
|
||||
public readonly layer: FilteredLayer
|
||||
public readonly tileIndex: number
|
||||
public readonly bbox: BBox
|
||||
private readonly upstream: FeatureSourceForLayer;
|
||||
private readonly upstream: FeatureSourceForLayer
|
||||
private readonly state: {
|
||||
locationControl: Store<{ zoom: number }>;
|
||||
selectedElement: Store<any>,
|
||||
globalFilters: Store<{ filter: FilterState }[]>,
|
||||
locationControl: Store<{ zoom: number }>
|
||||
selectedElement: Store<any>
|
||||
globalFilters: Store<{ filter: FilterState }[]>
|
||||
allElements: ElementStorage
|
||||
};
|
||||
private readonly _alreadyRegistered = new Set<UIEventSource<any>>();
|
||||
}
|
||||
private readonly _alreadyRegistered = new Set<UIEventSource<any>>()
|
||||
private readonly _is_dirty = new UIEventSource(false)
|
||||
private previousFeatureSet: Set<any> = undefined;
|
||||
private previousFeatureSet: Set<any> = undefined
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
locationControl: Store<{ zoom: number }>,
|
||||
selectedElement: Store<any>,
|
||||
allElements: ElementStorage,
|
||||
locationControl: Store<{ zoom: number }>
|
||||
selectedElement: Store<any>
|
||||
allElements: ElementStorage
|
||||
globalFilters: Store<{ filter: FilterState }[]>
|
||||
},
|
||||
tileIndex,
|
||||
|
@ -41,92 +42,95 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
this.upstream = upstream
|
||||
this.state = state
|
||||
|
||||
this.layer = upstream.layer;
|
||||
const layer = upstream.layer;
|
||||
const self = this;
|
||||
this.layer = upstream.layer
|
||||
const layer = upstream.layer
|
||||
const self = this
|
||||
upstream.features.addCallback(() => {
|
||||
self.update();
|
||||
});
|
||||
|
||||
|
||||
layer.appliedFilters.addCallback(_ => {
|
||||
self.update()
|
||||
})
|
||||
|
||||
this._is_dirty.stabilized(1000).addCallbackAndRunD(dirty => {
|
||||
layer.appliedFilters.addCallback((_) => {
|
||||
self.update()
|
||||
})
|
||||
|
||||
this._is_dirty.stabilized(1000).addCallbackAndRunD((dirty) => {
|
||||
if (dirty) {
|
||||
self.update()
|
||||
}
|
||||
})
|
||||
|
||||
metataggingUpdated?.addCallback(_ => {
|
||||
metataggingUpdated?.addCallback((_) => {
|
||||
self._is_dirty.setData(true)
|
||||
})
|
||||
|
||||
state.globalFilters.addCallback(_ => {
|
||||
|
||||
state.globalFilters.addCallback((_) => {
|
||||
self.update()
|
||||
})
|
||||
|
||||
this.update();
|
||||
this.update()
|
||||
}
|
||||
|
||||
private update() {
|
||||
const self = this;
|
||||
const layer = this.upstream.layer;
|
||||
const features: { feature: OsmFeature; freshness: Date }[] = (this.upstream.features.data ?? []);
|
||||
const includedFeatureIds = new Set<string>();
|
||||
const globalFilters = self.state.globalFilters.data.map(f => f.filter);
|
||||
const self = this
|
||||
const layer = this.upstream.layer
|
||||
const features: { feature: OsmFeature; freshness: Date }[] =
|
||||
this.upstream.features.data ?? []
|
||||
const includedFeatureIds = new Set<string>()
|
||||
const globalFilters = self.state.globalFilters.data.map((f) => f.filter)
|
||||
const newFeatures = (features ?? []).filter((f) => {
|
||||
|
||||
self.registerCallback(f.feature)
|
||||
|
||||
const isShown: TagsFilter = layer.layerDef.isShown;
|
||||
const tags = f.feature.properties;
|
||||
if (isShown !== undefined && !isShown.matchesProperties(tags) ) {
|
||||
return false;
|
||||
const isShown: TagsFilter = layer.layerDef.isShown
|
||||
const tags = f.feature.properties
|
||||
if (isShown !== undefined && !isShown.matchesProperties(tags)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const tagsFilter = Array.from(layer.appliedFilters?.data?.values() ?? [])
|
||||
for (const filter of tagsFilter) {
|
||||
const neededTags: TagsFilter = filter?.currentFilter
|
||||
if (neededTags !== undefined && !neededTags.matchesProperties(f.feature.properties)) {
|
||||
if (
|
||||
neededTags !== undefined &&
|
||||
!neededTags.matchesProperties(f.feature.properties)
|
||||
) {
|
||||
// Hidden by the filter on the layer itself - we want to hide it no matter what
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for (const filter of globalFilters) {
|
||||
const neededTags: TagsFilter = filter?.currentFilter
|
||||
if (neededTags !== undefined && !neededTags.matchesProperties(f.feature.properties)) {
|
||||
if (
|
||||
neededTags !== undefined &&
|
||||
!neededTags.matchesProperties(f.feature.properties)
|
||||
) {
|
||||
// Hidden by the filter on the layer itself - we want to hide it no matter what
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
includedFeatureIds.add(f.feature.properties.id)
|
||||
return true;
|
||||
});
|
||||
return true
|
||||
})
|
||||
|
||||
const previousSet = this.previousFeatureSet;
|
||||
const previousSet = this.previousFeatureSet
|
||||
this._is_dirty.setData(false)
|
||||
|
||||
// Is there any difference between the two sets?
|
||||
if (previousSet !== undefined && previousSet.size === includedFeatureIds.size) {
|
||||
// The size of the sets is the same - they _might_ be identical
|
||||
const newItemFound = Array.from(includedFeatureIds).some(id => !previousSet.has(id))
|
||||
const newItemFound = Array.from(includedFeatureIds).some((id) => !previousSet.has(id))
|
||||
if (!newItemFound) {
|
||||
// We know that:
|
||||
// We know that:
|
||||
// - The sets have the same size
|
||||
// - Every item from the new set has been found in the old set
|
||||
// which means they are identical!
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Something new has been found!
|
||||
this.features.setData(newFeatures);
|
||||
|
||||
this.features.setData(newFeatures)
|
||||
}
|
||||
|
||||
private registerCallback(feature: any) {
|
||||
|
@ -139,11 +143,10 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
}
|
||||
this._alreadyRegistered.add(src)
|
||||
|
||||
const self = this;
|
||||
const self = this
|
||||
// Add a callback as a changed tag migh change the filter
|
||||
src.addCallbackAndRunD(_ => {
|
||||
src.addCallbackAndRunD((_) => {
|
||||
self._is_dirty.setData(true)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,168 +1,163 @@
|
|||
/**
|
||||
* Fetches a geojson file somewhere and passes it along
|
||||
*/
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
import {GeoOperations} from "../../GeoOperations";
|
||||
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
|
||||
export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
||||
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
public readonly state = new UIEventSource<undefined | {error: string} | "loaded">(undefined)
|
||||
public readonly name;
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>
|
||||
public readonly state = new UIEventSource<undefined | { error: string } | "loaded">(undefined)
|
||||
public readonly name
|
||||
public readonly isOsmCache: boolean
|
||||
public readonly layer: FilteredLayer;
|
||||
public readonly layer: FilteredLayer
|
||||
public readonly tileIndex
|
||||
public readonly bbox;
|
||||
private readonly seenids: Set<string>;
|
||||
private readonly idKey ?: string;
|
||||
|
||||
public constructor(flayer: FilteredLayer,
|
||||
zxy?: [number, number, number] | BBox,
|
||||
options?: {
|
||||
featureIdBlacklist?: Set<string>
|
||||
}) {
|
||||
public readonly bbox
|
||||
private readonly seenids: Set<string>
|
||||
private readonly idKey?: string
|
||||
|
||||
public constructor(
|
||||
flayer: FilteredLayer,
|
||||
zxy?: [number, number, number] | BBox,
|
||||
options?: {
|
||||
featureIdBlacklist?: Set<string>
|
||||
}
|
||||
) {
|
||||
if (flayer.layerDef.source.geojsonZoomLevel !== undefined && zxy === undefined) {
|
||||
throw "Dynamic layers are not supported. Use 'DynamicGeoJsonTileSource instead"
|
||||
}
|
||||
|
||||
this.layer = flayer;
|
||||
this.layer = flayer
|
||||
this.idKey = flayer.layerDef.source.idKey
|
||||
this.seenids = options?.featureIdBlacklist ?? new Set<string>()
|
||||
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
|
||||
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id)
|
||||
if (zxy !== undefined) {
|
||||
let tile_bbox: BBox;
|
||||
let tile_bbox: BBox
|
||||
if (zxy instanceof BBox) {
|
||||
tile_bbox = zxy;
|
||||
tile_bbox = zxy
|
||||
} else {
|
||||
const [z, x, y] = zxy;
|
||||
tile_bbox = BBox.fromTile(z, x, y);
|
||||
const [z, x, y] = zxy
|
||||
tile_bbox = BBox.fromTile(z, x, y)
|
||||
|
||||
this.tileIndex = Tiles.tile_index(z, x, y)
|
||||
this.bbox = BBox.fromTile(z, x, y)
|
||||
url = url
|
||||
.replace('{z}', "" + z)
|
||||
.replace('{x}', "" + x)
|
||||
.replace('{y}', "" + y)
|
||||
.replace("{z}", "" + z)
|
||||
.replace("{x}", "" + x)
|
||||
.replace("{y}", "" + y)
|
||||
}
|
||||
let bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox
|
||||
let bounds: { minLat: number; maxLat: number; minLon: number; maxLon: number } =
|
||||
tile_bbox
|
||||
if (this.layer.layerDef.source.mercatorCrs) {
|
||||
bounds = tile_bbox.toMercator()
|
||||
}
|
||||
|
||||
url = url
|
||||
.replace('{y_min}', "" + bounds.minLat)
|
||||
.replace('{y_max}', "" + bounds.maxLat)
|
||||
.replace('{x_min}', "" + bounds.minLon)
|
||||
.replace('{x_max}', "" + bounds.maxLon)
|
||||
|
||||
|
||||
.replace("{y_min}", "" + bounds.minLat)
|
||||
.replace("{y_max}", "" + bounds.maxLat)
|
||||
.replace("{x_min}", "" + bounds.minLon)
|
||||
.replace("{x_max}", "" + bounds.maxLon)
|
||||
} else {
|
||||
this.tileIndex = Tiles.tile_index(0, 0, 0)
|
||||
this.bbox = BBox.global;
|
||||
this.bbox = BBox.global
|
||||
}
|
||||
|
||||
this.name = "GeoJsonSource of " + url;
|
||||
this.name = "GeoJsonSource of " + url
|
||||
|
||||
this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer;
|
||||
this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer
|
||||
this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
this.LoadJSONFrom(url)
|
||||
}
|
||||
|
||||
|
||||
private LoadJSONFrom(url: string) {
|
||||
const eventSource = this.features;
|
||||
const self = this;
|
||||
const eventSource = this.features
|
||||
const self = this
|
||||
Utils.downloadJsonCached(url, 60 * 60)
|
||||
.then(json => {
|
||||
.then((json) => {
|
||||
self.state.setData("loaded")
|
||||
// TODO: move somewhere else, just for testing
|
||||
// Check for maproulette data
|
||||
if (url.startsWith("https://maproulette.org/api/v2/tasks/box/")) {
|
||||
console.log("MapRoulette data detected")
|
||||
const data = json;
|
||||
let maprouletteFeatures: any[] = [];
|
||||
data.forEach(element => {
|
||||
const data = json
|
||||
let maprouletteFeatures: any[] = []
|
||||
data.forEach((element) => {
|
||||
maprouletteFeatures.push({
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [element.point.lng, element.point.lat]
|
||||
coordinates: [element.point.lng, element.point.lat],
|
||||
},
|
||||
properties: {
|
||||
// Map all properties to the feature
|
||||
...element,
|
||||
}
|
||||
});
|
||||
});
|
||||
json.features = maprouletteFeatures;
|
||||
},
|
||||
})
|
||||
})
|
||||
json.features = maprouletteFeatures
|
||||
}
|
||||
|
||||
if (json.features === undefined || json.features === null) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (self.layer.layerDef.source.mercatorCrs) {
|
||||
json = GeoOperations.GeoJsonToWGS84(json)
|
||||
}
|
||||
|
||||
const time = new Date();
|
||||
const newFeatures: { feature: any, freshness: Date } [] = []
|
||||
let i = 0;
|
||||
let skipped = 0;
|
||||
const time = new Date()
|
||||
const newFeatures: { feature: any; freshness: Date }[] = []
|
||||
let i = 0
|
||||
let skipped = 0
|
||||
for (const feature of json.features) {
|
||||
const props = feature.properties
|
||||
for (const key in props) {
|
||||
|
||||
if(props[key] === null){
|
||||
if (props[key] === null) {
|
||||
delete props[key]
|
||||
}
|
||||
|
||||
|
||||
if (typeof props[key] !== "string") {
|
||||
// Make sure all the values are string, it crashes stuff otherwise
|
||||
props[key] = JSON.stringify(props[key])
|
||||
}
|
||||
}
|
||||
|
||||
if(self.idKey !== undefined){
|
||||
if (self.idKey !== undefined) {
|
||||
props.id = props[self.idKey]
|
||||
}
|
||||
|
||||
|
||||
if (props.id === undefined) {
|
||||
props.id = url + "/" + i;
|
||||
feature.id = url + "/" + i;
|
||||
i++;
|
||||
props.id = url + "/" + i
|
||||
feature.id = url + "/" + i
|
||||
i++
|
||||
}
|
||||
if (self.seenids.has(props.id)) {
|
||||
skipped++;
|
||||
continue;
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
self.seenids.add(props.id)
|
||||
|
||||
let freshness: Date = time;
|
||||
let freshness: Date = time
|
||||
if (feature.properties["_last_edit:timestamp"] !== undefined) {
|
||||
freshness = new Date(props["_last_edit:timestamp"])
|
||||
}
|
||||
|
||||
newFeatures.push({feature: feature, freshness: freshness})
|
||||
newFeatures.push({ feature: feature, freshness: freshness })
|
||||
}
|
||||
|
||||
if (newFeatures.length == 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
eventSource.setData(eventSource.data.concat(newFeatures))
|
||||
|
||||
}).catch(msg => {
|
||||
console.debug("Could not load geojson layer", url, "due to", msg);
|
||||
self.state.setData({error: msg})
|
||||
})
|
||||
})
|
||||
.catch((msg) => {
|
||||
console.debug("Could not load geojson layer", url, "due to", msg)
|
||||
self.state.setData({ error: msg })
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,49 +1,50 @@
|
|||
import {Changes} from "../../Osm/Changes";
|
||||
import {OsmNode, OsmObject, OsmRelation, OsmWay} from "../../Osm/OsmObject";
|
||||
import FeatureSource from "../FeatureSource";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import {ChangeDescription} from "../../Osm/Actions/ChangeDescription";
|
||||
import {ElementStorage} from "../../ElementStorage";
|
||||
import { Changes } from "../../Osm/Changes"
|
||||
import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject"
|
||||
import FeatureSource from "../FeatureSource"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import { ChangeDescription } from "../../Osm/Actions/ChangeDescription"
|
||||
import { ElementStorage } from "../../ElementStorage"
|
||||
import { OsmId, OsmTags } from "../../../Models/OsmFeature"
|
||||
|
||||
export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
||||
// This class name truly puts the 'Java' into 'Javascript'
|
||||
|
||||
/**
|
||||
* A feature source containing exclusively new elements.
|
||||
*
|
||||
*
|
||||
* These elements are probably created by the 'SimpleAddUi' which generates a new point, but the import functionality might create a line or polygon too.
|
||||
* Other sources of new points are e.g. imports from nodes
|
||||
*/
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string = "newFeatures";
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> =
|
||||
new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
public readonly name: string = "newFeatures"
|
||||
|
||||
constructor(changes: Changes, allElementStorage: ElementStorage, backendUrl: string) {
|
||||
const seenChanges = new Set<ChangeDescription>()
|
||||
const features = this.features.data
|
||||
const self = this
|
||||
|
||||
const seenChanges = new Set<ChangeDescription>();
|
||||
const features = this.features.data;
|
||||
const self = this;
|
||||
|
||||
changes.pendingChanges.stabilized(100).addCallbackAndRunD(changes => {
|
||||
changes.pendingChanges.stabilized(100).addCallbackAndRunD((changes) => {
|
||||
if (changes.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let somethingChanged = false;
|
||||
const now = new Date()
|
||||
let somethingChanged = false
|
||||
|
||||
function add(feature) {
|
||||
feature.id = feature.properties.id
|
||||
features.push({
|
||||
feature: feature,
|
||||
freshness: now
|
||||
freshness: now,
|
||||
})
|
||||
somethingChanged = true;
|
||||
somethingChanged = true
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
if (seenChanges.has(change)) {
|
||||
// Already handled
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
seenChanges.add(change)
|
||||
|
||||
|
@ -59,38 +60,36 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
|||
// For this, we introspect the change
|
||||
if (allElementStorage.has(change.type + "/" + change.id)) {
|
||||
// The current point already exists, we don't have to do anything here
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
console.debug("Detected a reused point")
|
||||
// The 'allElementsStore' does _not_ have this point yet, so we have to create it
|
||||
OsmObject.DownloadObjectAsync(change.type + "/" + change.id).then(feat => {
|
||||
OsmObject.DownloadObjectAsync(change.type + "/" + change.id).then((feat) => {
|
||||
console.log("Got the reused point:", feat)
|
||||
for (const kv of change.tags) {
|
||||
feat.tags[kv.k] = kv.v
|
||||
}
|
||||
const geojson = feat.asGeoJson();
|
||||
const geojson = feat.asGeoJson()
|
||||
allElementStorage.addOrGetElement(geojson)
|
||||
self.features.data.push({feature: geojson, freshness: new Date()})
|
||||
self.features.data.push({ feature: geojson, freshness: new Date() })
|
||||
self.features.ping()
|
||||
})
|
||||
continue
|
||||
|
||||
|
||||
} else if (change.id < 0 && change.changes === undefined) {
|
||||
// The geometry is not described - not a new point
|
||||
if (change.id < 0) {
|
||||
console.error("WARNING: got a new point without geometry!")
|
||||
}
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const tags = {}
|
||||
const tags: OsmTags = {
|
||||
id: <OsmId>(change.type + "/" + change.id),
|
||||
}
|
||||
for (const kv of change.tags) {
|
||||
tags[kv.k] = kv.v
|
||||
}
|
||||
tags["id"] = change.type + "/" + change.id
|
||||
|
||||
tags["_backend"] = backendUrl
|
||||
|
||||
|
@ -102,30 +101,31 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
|||
n.lon = change.changes["lon"]
|
||||
const geojson = n.asGeoJson()
|
||||
add(geojson)
|
||||
break;
|
||||
break
|
||||
case "way":
|
||||
const w = new OsmWay(change.id)
|
||||
w.tags = tags
|
||||
w.nodes = change.changes["nodes"]
|
||||
w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [lat, lon])
|
||||
w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [
|
||||
lat,
|
||||
lon,
|
||||
])
|
||||
add(w.asGeoJson())
|
||||
break;
|
||||
break
|
||||
case "relation":
|
||||
const r = new OsmRelation(change.id)
|
||||
r.tags = tags
|
||||
r.members = change.changes["members"]
|
||||
add(r.asGeoJson())
|
||||
break;
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not generate a new geometry to render on screen for:", e)
|
||||
}
|
||||
|
||||
}
|
||||
if (somethingChanged) {
|
||||
self.features.ping()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,34 +2,36 @@
|
|||
* Every previously added point is remembered, but new points are added.
|
||||
* Data coming from upstream will always overwrite a previous value
|
||||
*/
|
||||
import FeatureSource, {Tiled} from "../FeatureSource";
|
||||
import {Store, UIEventSource} from "../../UIEventSource";
|
||||
import {BBox} from "../../BBox";
|
||||
import FeatureSource, { Tiled } from "../FeatureSource"
|
||||
import { Store, UIEventSource } from "../../UIEventSource"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
export default class RememberingSource implements FeatureSource, Tiled {
|
||||
|
||||
public readonly features: Store<{ feature: any, freshness: Date }[]>;
|
||||
public readonly name;
|
||||
public readonly features: Store<{ feature: any; freshness: Date }[]>
|
||||
public readonly name
|
||||
public readonly tileIndex: number
|
||||
public readonly bbox: BBox
|
||||
|
||||
constructor(source: FeatureSource & Tiled) {
|
||||
const self = this;
|
||||
this.name = "RememberingSource of " + source.name;
|
||||
const self = this
|
||||
this.name = "RememberingSource of " + source.name
|
||||
this.tileIndex = source.tileIndex
|
||||
this.bbox = source.bbox;
|
||||
this.bbox = source.bbox
|
||||
|
||||
const empty = [];
|
||||
const featureSource = new UIEventSource<{feature: any, freshness: Date}[]>(empty)
|
||||
const empty = []
|
||||
const featureSource = new UIEventSource<{ feature: any; freshness: Date }[]>(empty)
|
||||
this.features = featureSource
|
||||
source.features.addCallbackAndRunD(features => {
|
||||
const oldFeatures = self.features?.data ?? empty;
|
||||
source.features.addCallbackAndRunD((features) => {
|
||||
const oldFeatures = self.features?.data ?? empty
|
||||
// Then new ids
|
||||
const ids = new Set<string>(features.map(f => f.feature.properties.id + f.feature.geometry.type));
|
||||
const ids = new Set<string>(
|
||||
features.map((f) => f.feature.properties.id + f.feature.geometry.type)
|
||||
)
|
||||
// the old data
|
||||
const oldData = oldFeatures.filter(old => !ids.has(old.feature.properties.id + old.feature.geometry.type))
|
||||
const oldData = oldFeatures.filter(
|
||||
(old) => !ids.has(old.feature.properties.id + old.feature.geometry.type)
|
||||
)
|
||||
featureSource.setData([...features, ...oldData])
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +1,57 @@
|
|||
/**
|
||||
* This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indicates with what renderConfig it should be rendered.
|
||||
*/
|
||||
import {Store} from "../../UIEventSource";
|
||||
import {GeoOperations} from "../../GeoOperations";
|
||||
import FeatureSource from "../FeatureSource";
|
||||
import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig";
|
||||
import { Store } from "../../UIEventSource"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
import FeatureSource from "../FeatureSource"
|
||||
import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig"
|
||||
|
||||
export default class RenderingMultiPlexerFeatureSource {
|
||||
public readonly features: Store<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>;
|
||||
private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[];
|
||||
private centroidRenderings: { rendering: PointRenderingConfig; index: number }[];
|
||||
private projectedCentroidRenderings: { rendering: PointRenderingConfig; index: number }[];
|
||||
private startRenderings: { rendering: PointRenderingConfig; index: number }[];
|
||||
private endRenderings: { rendering: PointRenderingConfig; index: number }[];
|
||||
private hasCentroid: boolean;
|
||||
private lineRenderObjects: LineRenderingConfig[];
|
||||
public readonly features: Store<
|
||||
(any & {
|
||||
pointRenderingIndex: number | undefined
|
||||
lineRenderingIndex: number | undefined
|
||||
})[]
|
||||
>
|
||||
private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private centroidRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private projectedCentroidRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private startRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private endRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private hasCentroid: boolean
|
||||
private lineRenderObjects: LineRenderingConfig[]
|
||||
|
||||
|
||||
private inspectFeature(feat, addAsPoint: (feat, rendering, centerpoint: [number, number]) => void, withIndex: any[]){
|
||||
private inspectFeature(
|
||||
feat,
|
||||
addAsPoint: (feat, rendering, centerpoint: [number, number]) => void,
|
||||
withIndex: any[]
|
||||
) {
|
||||
if (feat.geometry.type === "Point") {
|
||||
|
||||
for (const rendering of this.pointRenderings) {
|
||||
withIndex.push({
|
||||
...feat,
|
||||
pointRenderingIndex: rendering.index
|
||||
pointRenderingIndex: rendering.index,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// This is a a line: add the centroids
|
||||
let centerpoint: [number, number] = undefined;
|
||||
let projectedCenterPoint : [number, number] = undefined
|
||||
if(this.hasCentroid){
|
||||
centerpoint = GeoOperations.centerpointCoordinates(feat)
|
||||
if(this.projectedCentroidRenderings.length > 0){
|
||||
projectedCenterPoint = <[number,number]> GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates
|
||||
let centerpoint: [number, number] = undefined
|
||||
let projectedCenterPoint: [number, number] = undefined
|
||||
if (this.hasCentroid) {
|
||||
centerpoint = GeoOperations.centerpointCoordinates(feat)
|
||||
if (this.projectedCentroidRenderings.length > 0) {
|
||||
projectedCenterPoint = <[number, number]>(
|
||||
GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates
|
||||
)
|
||||
}
|
||||
}
|
||||
for (const rendering of this.centroidRenderings) {
|
||||
addAsPoint(feat, rendering, centerpoint)
|
||||
}
|
||||
|
||||
|
||||
if (feat.geometry.type === "LineString") {
|
||||
|
||||
for (const rendering of this.projectedCentroidRenderings) {
|
||||
addAsPoint(feat, rendering, projectedCenterPoint)
|
||||
}
|
||||
|
@ -58,73 +65,69 @@ export default class RenderingMultiPlexerFeatureSource {
|
|||
const coordinate = coordinates[coordinates.length - 1]
|
||||
addAsPoint(feat, rendering, coordinate)
|
||||
}
|
||||
|
||||
}else{
|
||||
} else {
|
||||
for (const rendering of this.projectedCentroidRenderings) {
|
||||
addAsPoint(feat, rendering, centerpoint)
|
||||
}
|
||||
}
|
||||
|
||||
// AT last, add it 'as is' to what we should render
|
||||
// AT last, add it 'as is' to what we should render
|
||||
for (let i = 0; i < this.lineRenderObjects.length; i++) {
|
||||
withIndex.push({
|
||||
...feat,
|
||||
lineRenderingIndex: i
|
||||
lineRenderingIndex: i,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
constructor(upstream: FeatureSource, layer: LayerConfig) {
|
||||
|
||||
const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({
|
||||
rendering: r,
|
||||
index: i
|
||||
}))
|
||||
this.pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point"))
|
||||
this.centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid"))
|
||||
this.projectedCentroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("projected_centerpoint"))
|
||||
this.startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start"))
|
||||
this.endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end"))
|
||||
this.hasCentroid = this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0
|
||||
const pointRenderObjects: { rendering: PointRenderingConfig; index: number }[] =
|
||||
layer.mapRendering.map((r, i) => ({
|
||||
rendering: r,
|
||||
index: i,
|
||||
}))
|
||||
this.pointRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("point"))
|
||||
this.centroidRenderings = pointRenderObjects.filter((r) =>
|
||||
r.rendering.location.has("centroid")
|
||||
)
|
||||
this.projectedCentroidRenderings = pointRenderObjects.filter((r) =>
|
||||
r.rendering.location.has("projected_centerpoint")
|
||||
)
|
||||
this.startRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("start"))
|
||||
this.endRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("end"))
|
||||
this.hasCentroid =
|
||||
this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0
|
||||
this.lineRenderObjects = layer.lineRendering
|
||||
|
||||
this.features = upstream.features.map(
|
||||
features => {
|
||||
if (features === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
const withIndex: any[] = [];
|
||||
|
||||
function addAsPoint(feat, rendering, coordinate) {
|
||||
const patched = {
|
||||
...feat,
|
||||
pointRenderingIndex: rendering.index
|
||||
}
|
||||
patched.geometry = {
|
||||
type: "Point",
|
||||
coordinates: coordinate
|
||||
}
|
||||
withIndex.push(patched)
|
||||
}
|
||||
|
||||
|
||||
for (const f of features) {
|
||||
const feat = f.feature;
|
||||
if(feat === undefined){
|
||||
continue
|
||||
}
|
||||
this.inspectFeature(feat, addAsPoint, withIndex)
|
||||
}
|
||||
|
||||
|
||||
return withIndex;
|
||||
this.features = upstream.features.map((features) => {
|
||||
if (features === undefined) {
|
||||
return undefined
|
||||
}
|
||||
);
|
||||
|
||||
const withIndex: any[] = []
|
||||
|
||||
function addAsPoint(feat, rendering, coordinate) {
|
||||
const patched = {
|
||||
...feat,
|
||||
pointRenderingIndex: rendering.index,
|
||||
}
|
||||
patched.geometry = {
|
||||
type: "Point",
|
||||
coordinates: coordinate,
|
||||
}
|
||||
withIndex.push(patched)
|
||||
}
|
||||
|
||||
for (const f of features) {
|
||||
const feat = f.feature
|
||||
if (feat === undefined) {
|
||||
continue
|
||||
}
|
||||
this.inspectFeature(feat, addAsPoint, withIndex)
|
||||
}
|
||||
|
||||
return withIndex
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {BBox} from "../../BBox";
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
public readonly name: string = "SimpleFeatureSource";
|
||||
public readonly layer: FilteredLayer;
|
||||
public readonly bbox: BBox = BBox.global;
|
||||
public readonly tileIndex: number;
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>
|
||||
public readonly name: string = "SimpleFeatureSource"
|
||||
public readonly layer: FilteredLayer
|
||||
public readonly bbox: BBox = BBox.global
|
||||
public readonly tileIndex: number
|
||||
|
||||
constructor(layer: FilteredLayer, tileIndex: number, featureSource?: UIEventSource<{ feature: any; freshness: Date }[]> ) {
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
tileIndex: number,
|
||||
featureSource?: UIEventSource<{ feature: any; freshness: Date }[]>
|
||||
) {
|
||||
this.name = "SimpleFeatureSource(" + layer.layerDef.id + ")"
|
||||
this.layer = layer
|
||||
this.tileIndex = tileIndex ?? 0;
|
||||
this.tileIndex = tileIndex ?? 0
|
||||
this.bbox = BBox.fromTileIndex(this.tileIndex)
|
||||
this.features = featureSource ?? new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
this.features = featureSource ?? new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,62 +1,90 @@
|
|||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {ImmutableStore, Store, UIEventSource} from "../../UIEventSource";
|
||||
import {stat} from "fs";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {BBox} from "../../BBox";
|
||||
import {Feature} from "@turf/turf";
|
||||
import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
|
||||
import { stat } from "fs"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { BBox } from "../../BBox"
|
||||
import { Feature } from "@turf/turf"
|
||||
|
||||
/**
|
||||
* A simple, read only feature store.
|
||||
*/
|
||||
export default class StaticFeatureSource implements FeatureSource {
|
||||
public readonly features: Store<{ feature: any; freshness: Date }[]>;
|
||||
public readonly features: Store<{ feature: any; freshness: Date }[]>
|
||||
public readonly name: string
|
||||
|
||||
constructor(features: Store<{ feature: Feature, freshness: Date }[]>, name = "StaticFeatureSource") {
|
||||
constructor(
|
||||
features: Store<{ feature: Feature; freshness: Date }[]>,
|
||||
name = "StaticFeatureSource"
|
||||
) {
|
||||
if (features === undefined) {
|
||||
throw "Static feature source received undefined as source"
|
||||
}
|
||||
this.name = name;
|
||||
this.features = features;
|
||||
this.name = name
|
||||
this.features = features
|
||||
}
|
||||
|
||||
public static fromGeojsonAndDate(features: { feature: Feature, freshness: Date }[], name = "StaticFeatureSourceFromGeojsonAndDate"): StaticFeatureSource {
|
||||
return new StaticFeatureSource(new ImmutableStore(features), name);
|
||||
public static fromGeojsonAndDate(
|
||||
features: { feature: Feature; freshness: Date }[],
|
||||
name = "StaticFeatureSourceFromGeojsonAndDate"
|
||||
): StaticFeatureSource {
|
||||
return new StaticFeatureSource(new ImmutableStore(features), name)
|
||||
}
|
||||
|
||||
|
||||
public static fromGeojson(geojson: Feature[], name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource {
|
||||
const now = new Date();
|
||||
return StaticFeatureSource.fromGeojsonAndDate(geojson.map(feature => ({feature, freshness: now})), name);
|
||||
public static fromGeojson(
|
||||
geojson: Feature[],
|
||||
name = "StaticFeatureSourceFromGeojson"
|
||||
): StaticFeatureSource {
|
||||
const now = new Date()
|
||||
return StaticFeatureSource.fromGeojsonAndDate(
|
||||
geojson.map((feature) => ({ feature, freshness: now })),
|
||||
name
|
||||
)
|
||||
}
|
||||
|
||||
public static fromGeojsonStore(geojson: Store<Feature[]>, name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource {
|
||||
const now = new Date();
|
||||
const mapped : Store<{feature: Feature, freshness: Date}[]> = geojson.map(features => features.map(feature => ({feature, freshness: now})))
|
||||
return new StaticFeatureSource(mapped, name);
|
||||
public static fromGeojsonStore(
|
||||
geojson: Store<Feature[]>,
|
||||
name = "StaticFeatureSourceFromGeojson"
|
||||
): StaticFeatureSource {
|
||||
const now = new Date()
|
||||
const mapped: Store<{ feature: Feature; freshness: Date }[]> = geojson.map((features) =>
|
||||
features.map((feature) => ({ feature, freshness: now }))
|
||||
)
|
||||
return new StaticFeatureSource(mapped, name)
|
||||
}
|
||||
|
||||
static fromDateless(featureSource: Store<{ feature: Feature }[]>, name = "StaticFeatureSourceFromDateless") {
|
||||
const now = new Date();
|
||||
return new StaticFeatureSource(featureSource.map(features => features.map(feature => ({
|
||||
feature: feature.feature,
|
||||
freshness: now
|
||||
}))), name);
|
||||
static fromDateless(
|
||||
featureSource: Store<{ feature: Feature }[]>,
|
||||
name = "StaticFeatureSourceFromDateless"
|
||||
) {
|
||||
const now = new Date()
|
||||
return new StaticFeatureSource(
|
||||
featureSource.map((features) =>
|
||||
features.map((feature) => ({
|
||||
feature: feature.feature,
|
||||
freshness: now,
|
||||
}))
|
||||
),
|
||||
name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class TiledStaticFeatureSource extends StaticFeatureSource implements Tiled, FeatureSourceForLayer{
|
||||
export class TiledStaticFeatureSource
|
||||
extends StaticFeatureSource
|
||||
implements Tiled, FeatureSourceForLayer
|
||||
{
|
||||
public readonly bbox: BBox = BBox.global
|
||||
public readonly tileIndex: number
|
||||
public readonly layer: FilteredLayer
|
||||
|
||||
public readonly bbox: BBox = BBox.global;
|
||||
public readonly tileIndex: number;
|
||||
public readonly layer: FilteredLayer;
|
||||
|
||||
constructor(features: Store<{ feature: any, freshness: Date }[]>, layer: FilteredLayer ,tileIndex : number = 0) {
|
||||
super(features);
|
||||
this.tileIndex = tileIndex ;
|
||||
this.layer= layer;
|
||||
constructor(
|
||||
features: Store<{ feature: any; freshness: Date }[]>,
|
||||
layer: FilteredLayer,
|
||||
tileIndex: number = 0
|
||||
) {
|
||||
super(features)
|
||||
this.tileIndex = tileIndex
|
||||
this.layer = layer
|
||||
this.bbox = BBox.fromTileIndex(this.tileIndex)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import {Tiles} from "../../Models/TileRange";
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
|
||||
export default class TileFreshnessCalculator {
|
||||
|
||||
/**
|
||||
* All the freshnesses per tile index
|
||||
* @private
|
||||
*/
|
||||
private readonly freshnesses = new Map<number, Date>();
|
||||
private readonly freshnesses = new Map<number, Date>()
|
||||
|
||||
/**
|
||||
* Marks that some data got loaded for this layer
|
||||
|
@ -16,14 +15,14 @@ export default class TileFreshnessCalculator {
|
|||
public addTileLoad(tileId: number, freshness: Date) {
|
||||
const existingFreshness = this.freshnessFor(...Tiles.tile_from_index(tileId))
|
||||
if (existingFreshness >= freshness) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this.freshnesses.set(tileId, freshness)
|
||||
|
||||
// Do we have freshness for the neighbouring tiles? If so, we can mark the tile above as loaded too!
|
||||
let [z, x, y] = Tiles.tile_from_index(tileId)
|
||||
if (z === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
x = x - (x % 2) // Make the tiles always even
|
||||
y = y - (y % 2)
|
||||
|
@ -48,11 +47,7 @@ export default class TileFreshnessCalculator {
|
|||
const leastFresh = Math.min(ul, ur, ll, lr)
|
||||
const date = new Date()
|
||||
date.setTime(leastFresh)
|
||||
this.addTileLoad(
|
||||
Tiles.tile_index(z - 1, Math.floor(x / 2), Math.floor(y / 2)),
|
||||
date
|
||||
)
|
||||
|
||||
this.addTileLoad(Tiles.tile_index(z - 1, Math.floor(x / 2), Math.floor(y / 2)), date)
|
||||
}
|
||||
|
||||
public freshnessFor(z: number, x: number, y: number): Date {
|
||||
|
@ -65,7 +60,5 @@ export default class TileFreshnessCalculator {
|
|||
}
|
||||
// recurse up
|
||||
return this.freshnessFor(z - 1, Math.floor(x / 2), Math.floor(y / 2))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import DynamicTileSource from "./DynamicTileSource";
|
||||
import {Utils} from "../../../Utils";
|
||||
import GeoJsonSource from "../Sources/GeoJsonSource";
|
||||
import {BBox} from "../../BBox";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import DynamicTileSource from "./DynamicTileSource"
|
||||
import { Utils } from "../../../Utils"
|
||||
import GeoJsonSource from "../Sources/GeoJsonSource"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
||||
|
||||
private static whitelistCache = new Map<string, any>()
|
||||
|
||||
constructor(layer: FilteredLayer,
|
||||
registerLayer: (layer: FeatureSourceForLayer & Tiled) => void,
|
||||
state: {
|
||||
locationControl?: UIEventSource<{zoom?: number}>
|
||||
currentBounds: UIEventSource<BBox>
|
||||
}) {
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
registerLayer: (layer: FeatureSourceForLayer & Tiled) => void,
|
||||
state: {
|
||||
locationControl?: UIEventSource<{ zoom?: number }>
|
||||
currentBounds: UIEventSource<BBox>
|
||||
}
|
||||
) {
|
||||
const source = layer.layerDef.source
|
||||
if (source.geojsonZoomLevel === undefined) {
|
||||
throw "Invalid layer: geojsonZoomLevel expected"
|
||||
|
@ -26,7 +27,6 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
|||
|
||||
let whitelist = undefined
|
||||
if (source.geojsonSource.indexOf("{x}_{y}.geojson") > 0) {
|
||||
|
||||
const whitelistUrl = source.geojsonSource
|
||||
.replace("{z}", "" + source.geojsonZoomLevel)
|
||||
.replace("{x}_{y}.geojson", "overview.json")
|
||||
|
@ -35,26 +35,33 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
|||
if (DynamicGeoJsonTileSource.whitelistCache.has(whitelistUrl)) {
|
||||
whitelist = DynamicGeoJsonTileSource.whitelistCache.get(whitelistUrl)
|
||||
} else {
|
||||
Utils.downloadJsonCached(whitelistUrl, 1000 * 60 * 60).then(
|
||||
json => {
|
||||
const data = new Map<number, Set<number>>();
|
||||
Utils.downloadJsonCached(whitelistUrl, 1000 * 60 * 60)
|
||||
.then((json) => {
|
||||
const data = new Map<number, Set<number>>()
|
||||
for (const x in json) {
|
||||
if (x === "zoom") {
|
||||
continue
|
||||
}
|
||||
data.set(Number(x), new Set(json[x]))
|
||||
}
|
||||
console.log("The whitelist is", data, "based on ", json, "from", whitelistUrl)
|
||||
console.log(
|
||||
"The whitelist is",
|
||||
data,
|
||||
"based on ",
|
||||
json,
|
||||
"from",
|
||||
whitelistUrl
|
||||
)
|
||||
whitelist = data
|
||||
DynamicGeoJsonTileSource.whitelistCache.set(whitelistUrl, whitelist)
|
||||
}
|
||||
).catch(err => {
|
||||
console.warn("No whitelist found for ", layer.layerDef.id, err)
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("No whitelist found for ", layer.layerDef.id, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const blackList = (new Set<string>())
|
||||
const blackList = new Set<string>()
|
||||
super(
|
||||
layer,
|
||||
source.geojsonZoomLevel,
|
||||
|
@ -62,29 +69,28 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
|||
if (whitelist !== undefined) {
|
||||
const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2])
|
||||
if (!isWhiteListed) {
|
||||
console.debug("Not downloading tile", ...zxy, "as it is not on the whitelist")
|
||||
return undefined;
|
||||
console.debug(
|
||||
"Not downloading tile",
|
||||
...zxy,
|
||||
"as it is not on the whitelist"
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const src = new GeoJsonSource(
|
||||
layer,
|
||||
zxy,
|
||||
{
|
||||
featureIdBlacklist: blackList
|
||||
}
|
||||
)
|
||||
|
||||
const src = new GeoJsonSource(layer, zxy, {
|
||||
featureIdBlacklist: blackList,
|
||||
})
|
||||
|
||||
registerLayer(src)
|
||||
return src
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
public static RegisterWhitelist(url: string, json: any) {
|
||||
const data = new Map<number, Set<number>>();
|
||||
const data = new Map<number, Set<number>>()
|
||||
for (const x in json) {
|
||||
if (x === "zoom") {
|
||||
continue
|
||||
|
@ -93,5 +99,4 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
|||
}
|
||||
DynamicGeoJsonTileSource.whitelistCache.set(url, data)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +1,80 @@
|
|||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import TileHierarchy from "./TileHierarchy";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import TileHierarchy from "./TileHierarchy"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
/***
|
||||
* A tiled source which dynamically loads the required tiles at a fixed zoom level
|
||||
*/
|
||||
export default class DynamicTileSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
|
||||
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>;
|
||||
private readonly _loadedTiles = new Set<number>();
|
||||
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>
|
||||
private readonly _loadedTiles = new Set<number>()
|
||||
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
zoomlevel: number,
|
||||
constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled),
|
||||
constructTile: (zxy: [number, number, number]) => FeatureSourceForLayer & Tiled,
|
||||
state: {
|
||||
currentBounds: UIEventSource<BBox>;
|
||||
locationControl?: UIEventSource<{zoom?: number}>
|
||||
currentBounds: UIEventSource<BBox>
|
||||
locationControl?: UIEventSource<{ zoom?: number }>
|
||||
}
|
||||
) {
|
||||
const self = this;
|
||||
const self = this
|
||||
|
||||
this.loadedTiles = new Map<number, FeatureSourceForLayer & Tiled>()
|
||||
const neededTiles = state.currentBounds.map(
|
||||
bounds => {
|
||||
if (bounds === undefined) {
|
||||
// We'll retry later
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) {
|
||||
// No need to download! - the layer is disabled
|
||||
return undefined;
|
||||
}
|
||||
const neededTiles = state.currentBounds
|
||||
.map(
|
||||
(bounds) => {
|
||||
if (bounds === undefined) {
|
||||
// We'll retry later
|
||||
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;
|
||||
}
|
||||
if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) {
|
||||
// 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
|
||||
}
|
||||
if (
|
||||
state.locationControl?.data?.zoom !== undefined &&
|
||||
state.locationControl.data.zoom < layer.layerDef.minzoom
|
||||
) {
|
||||
// No need to download! - the layer is disabled
|
||||
return undefined
|
||||
}
|
||||
|
||||
const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i))
|
||||
if (needed.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return needed
|
||||
}
|
||||
, [layer.isDisplayed, state.locationControl]).stabilized(250);
|
||||
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
|
||||
}
|
||||
|
||||
neededTiles.addCallbackAndRunD(neededIndexes => {
|
||||
const needed = Tiles.MapRange(tileRange, (x, y) =>
|
||||
Tiles.tile_index(zoomlevel, x, y)
|
||||
).filter((i) => !self._loadedTiles.has(i))
|
||||
if (needed.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return needed
|
||||
},
|
||||
[layer.isDisplayed, state.locationControl]
|
||||
)
|
||||
.stabilized(250)
|
||||
|
||||
neededTiles.addCallbackAndRunD((neededIndexes) => {
|
||||
console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes)
|
||||
if (neededIndexes === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
for (const neededIndex of neededIndexes) {
|
||||
self._loadedTiles.add(neededIndex)
|
||||
|
@ -68,10 +84,5 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,30 +1,26 @@
|
|||
import TileHierarchy from "./TileHierarchy";
|
||||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject";
|
||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
|
||||
import TileHierarchy from "./TileHierarchy"
|
||||
import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject"
|
||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
|
||||
export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> {
|
||||
public readonly loadedTiles = new Map<number, FeatureSource & Tiled>()
|
||||
private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void;
|
||||
private readonly onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void
|
||||
private readonly layer: FilteredLayer
|
||||
private readonly nodeByIds = new Map<number, OsmNode>();
|
||||
private readonly nodeByIds = new Map<number, OsmNode>()
|
||||
private readonly parentWays = new Map<number, UIEventSource<OsmWay[]>>()
|
||||
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
onTileLoaded: ((tile: Tiled & FeatureSourceForLayer) => void)) {
|
||||
constructor(layer: FilteredLayer, onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void) {
|
||||
this.onTileLoaded = onTileLoaded
|
||||
this.layer = layer;
|
||||
this.layer = layer
|
||||
if (this.layer === undefined) {
|
||||
throw "Layer is undefined"
|
||||
}
|
||||
}
|
||||
|
||||
public handleOsmJson(osmJson: any, tileId: number) {
|
||||
|
||||
const allObjects = OsmObject.ParseObjects(osmJson.elements)
|
||||
const nodesById = new Map<number, OsmNode>()
|
||||
|
||||
|
@ -32,7 +28,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
|||
if (osmObj.type !== "node") {
|
||||
continue
|
||||
}
|
||||
const osmNode = <OsmNode>osmObj;
|
||||
const osmNode = <OsmNode>osmObj
|
||||
nodesById.set(osmNode.id, osmNode)
|
||||
this.nodeByIds.set(osmNode.id, osmNode)
|
||||
}
|
||||
|
@ -41,33 +37,32 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
|||
if (osmObj.type !== "way") {
|
||||
continue
|
||||
}
|
||||
const osmWay = <OsmWay>osmObj;
|
||||
const osmWay = <OsmWay>osmObj
|
||||
for (const nodeId of osmWay.nodes) {
|
||||
|
||||
if (!this.parentWays.has(nodeId)) {
|
||||
const src = new UIEventSource<OsmWay[]>([])
|
||||
this.parentWays.set(nodeId, src)
|
||||
src.addCallback(parentWays => {
|
||||
src.addCallback((parentWays) => {
|
||||
const tgs = nodesById.get(nodeId).tags
|
||||
tgs ["parent_ways"] = JSON.stringify(parentWays.map(w => w.tags))
|
||||
tgs["parent_way_ids"] = JSON.stringify(parentWays.map(w => w.id))
|
||||
tgs["parent_ways"] = JSON.stringify(parentWays.map((w) => w.tags))
|
||||
tgs["parent_way_ids"] = JSON.stringify(parentWays.map((w) => w.id))
|
||||
})
|
||||
}
|
||||
const src = this.parentWays.get(nodeId)
|
||||
src.data.push(osmWay)
|
||||
src.ping();
|
||||
src.ping()
|
||||
}
|
||||
}
|
||||
const now = new Date()
|
||||
const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({
|
||||
feature: osmNode.asGeoJson(), freshness: now
|
||||
const asGeojsonFeatures = Array.from(nodesById.values()).map((osmNode) => ({
|
||||
feature: osmNode.asGeoJson(),
|
||||
freshness: now,
|
||||
}))
|
||||
|
||||
const featureSource = new SimpleFeatureSource(this.layer, tileId)
|
||||
featureSource.features.setData(asGeojsonFeatures)
|
||||
this.loadedTiles.set(tileId, featureSource)
|
||||
this.onTileLoaded(featureSource)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,6 +83,4 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
|||
public GetParentWays(nodeId: number): UIEventSource<OsmWay[]> {
|
||||
return this.parentWays.get(nodeId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import {Utils} from "../../../Utils";
|
||||
import * as OsmToGeoJson from "osmtogeojson";
|
||||
import StaticFeatureSource from "../Sources/StaticFeatureSource";
|
||||
import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter";
|
||||
import {Store, UIEventSource} from "../../UIEventSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import {Or} from "../../Tags/Or";
|
||||
import {TagsFilter} from "../../Tags/TagsFilter";
|
||||
import {OsmObject} from "../../Osm/OsmObject";
|
||||
import {FeatureCollection} from "@turf/turf";
|
||||
import { Utils } from "../../../Utils"
|
||||
import * as OsmToGeoJson from "osmtogeojson"
|
||||
import StaticFeatureSource from "../Sources/StaticFeatureSource"
|
||||
import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter"
|
||||
import { Store, UIEventSource } from "../../UIEventSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
|
||||
import { Or } from "../../Tags/Or"
|
||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||
import { OsmObject } from "../../Osm/OsmObject"
|
||||
import { FeatureCollection } from "@turf/turf"
|
||||
|
||||
/**
|
||||
* If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile'
|
||||
|
@ -20,67 +20,70 @@ export default class OsmFeatureSource {
|
|||
public readonly isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
public readonly downloadedTiles = new Set<number>()
|
||||
public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = []
|
||||
private readonly _backend: string;
|
||||
private readonly filteredLayers: Store<FilteredLayer[]>;
|
||||
private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void;
|
||||
private isActive: Store<boolean>;
|
||||
private readonly _backend: string
|
||||
private readonly filteredLayers: Store<FilteredLayer[]>
|
||||
private readonly handleTile: (fs: FeatureSourceForLayer & Tiled) => void
|
||||
private isActive: Store<boolean>
|
||||
private options: {
|
||||
handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
|
||||
isActive: Store<boolean>,
|
||||
neededTiles: Store<number[]>,
|
||||
handleTile: (tile: FeatureSourceForLayer & Tiled) => void
|
||||
isActive: Store<boolean>
|
||||
neededTiles: Store<number[]>
|
||||
markTileVisited?: (tileId: number) => void
|
||||
};
|
||||
private readonly allowedTags: TagsFilter;
|
||||
}
|
||||
private readonly allowedTags: TagsFilter
|
||||
|
||||
/**
|
||||
*
|
||||
* @param options: allowedFeatures is normally calculated from the layoutToUse
|
||||
*/
|
||||
constructor(options: {
|
||||
handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
|
||||
isActive: Store<boolean>,
|
||||
neededTiles: Store<number[]>,
|
||||
handleTile: (tile: FeatureSourceForLayer & Tiled) => void
|
||||
isActive: Store<boolean>
|
||||
neededTiles: Store<number[]>
|
||||
state: {
|
||||
readonly filteredLayers: UIEventSource<FilteredLayer[]>;
|
||||
readonly filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
readonly osmConnection: {
|
||||
Backend(): string
|
||||
};
|
||||
}
|
||||
readonly layoutToUse?: LayoutConfig
|
||||
},
|
||||
readonly allowedFeatures?: TagsFilter,
|
||||
}
|
||||
readonly allowedFeatures?: TagsFilter
|
||||
markTileVisited?: (tileId: number) => void
|
||||
}) {
|
||||
this.options = options;
|
||||
this._backend = options.state.osmConnection.Backend();
|
||||
this.filteredLayers = options.state.filteredLayers.map(layers => layers.filter(layer => layer.layerDef.source.geojsonSource === undefined))
|
||||
this.options = options
|
||||
this._backend = options.state.osmConnection.Backend()
|
||||
this.filteredLayers = options.state.filteredLayers.map((layers) =>
|
||||
layers.filter((layer) => layer.layerDef.source.geojsonSource === undefined)
|
||||
)
|
||||
this.handleTile = options.handleTile
|
||||
this.isActive = options.isActive
|
||||
const self = this
|
||||
options.neededTiles.addCallbackAndRunD(neededTiles => {
|
||||
options.neededTiles.addCallbackAndRunD((neededTiles) => {
|
||||
self.Update(neededTiles)
|
||||
})
|
||||
|
||||
|
||||
const neededLayers = (options.state.layoutToUse?.layers ?? [])
|
||||
.filter(layer => !layer.doNotDownload)
|
||||
.filter(layer => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer)
|
||||
this.allowedTags = options.allowedFeatures ?? new Or(neededLayers.map(l => l.source.osmTags))
|
||||
.filter((layer) => !layer.doNotDownload)
|
||||
.filter(
|
||||
(layer) => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer
|
||||
)
|
||||
this.allowedTags =
|
||||
options.allowedFeatures ?? new Or(neededLayers.map((l) => l.source.osmTags))
|
||||
}
|
||||
|
||||
private async Update(neededTiles: number[]) {
|
||||
if (this.options.isActive?.data === false) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
neededTiles = neededTiles.filter(tile => !this.downloadedTiles.has(tile))
|
||||
neededTiles = neededTiles.filter((tile) => !this.downloadedTiles.has(tile))
|
||||
|
||||
if (neededTiles.length == 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
this.isRunning.setData(true)
|
||||
try {
|
||||
|
||||
for (const neededTile of neededTiles) {
|
||||
this.downloadedTiles.add(neededTile)
|
||||
await this.LoadTile(...Tiles.tile_from_index(neededTile))
|
||||
|
@ -98,24 +101,30 @@ export default class OsmFeatureSource {
|
|||
* This method will download the full relation and return it as geojson if it was incomplete.
|
||||
* If the feature is already complete (or is not a relation), the feature will be returned
|
||||
*/
|
||||
private async patchIncompleteRelations(feature: {properties: {id: string}},
|
||||
originalJson: {elements: {type: "node" | "way" | "relation", id: number, } []}): Promise<any> {
|
||||
if(!feature.properties.id.startsWith("relation")){
|
||||
private async patchIncompleteRelations(
|
||||
feature: { properties: { id: string } },
|
||||
originalJson: { elements: { type: "node" | "way" | "relation"; id: number }[] }
|
||||
): Promise<any> {
|
||||
if (!feature.properties.id.startsWith("relation")) {
|
||||
return feature
|
||||
}
|
||||
const relationSpec = originalJson.elements.find(f => "relation/"+f.id === feature.properties.id)
|
||||
const members : {type: string, ref: number}[] = relationSpec["members"]
|
||||
const relationSpec = originalJson.elements.find(
|
||||
(f) => "relation/" + f.id === feature.properties.id
|
||||
)
|
||||
const members: { type: string; ref: number }[] = relationSpec["members"]
|
||||
for (const member of members) {
|
||||
const isFound = originalJson.elements.some(f => f.id === member.ref && f.type === member.type)
|
||||
const isFound = originalJson.elements.some(
|
||||
(f) => f.id === member.ref && f.type === member.type
|
||||
)
|
||||
if (isFound) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// This member is missing. We redownload the entire relation instead
|
||||
console.debug("Fetching incomplete relation "+feature.properties.id)
|
||||
console.debug("Fetching incomplete relation " + feature.properties.id)
|
||||
return (await OsmObject.DownloadObjectAsync(feature.properties.id)).asGeoJson()
|
||||
}
|
||||
return feature;
|
||||
return feature
|
||||
}
|
||||
|
||||
private async LoadTile(z, x, y): Promise<void> {
|
||||
|
@ -130,52 +139,69 @@ export default class OsmFeatureSource {
|
|||
const bbox = BBox.fromTile(z, x, y)
|
||||
const url = `${this._backend}/api/0.6/map?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}`
|
||||
|
||||
let error = undefined;
|
||||
let error = undefined
|
||||
try {
|
||||
const osmJson = await Utils.downloadJson(url)
|
||||
try {
|
||||
|
||||
console.log("Got tile", z, x, y, "from the osm api")
|
||||
this.rawDataHandlers.forEach(handler => handler(osmJson, Tiles.tile_index(z, x, y)))
|
||||
const geojson = <FeatureCollection<any , {id: string}>> OsmToGeoJson.default(osmJson,
|
||||
this.rawDataHandlers.forEach((handler) =>
|
||||
handler(osmJson, Tiles.tile_index(z, x, y))
|
||||
)
|
||||
const geojson = <FeatureCollection<any, { id: string }>>OsmToGeoJson.default(
|
||||
osmJson,
|
||||
// @ts-ignore
|
||||
{
|
||||
flatProperties: true
|
||||
});
|
||||
|
||||
flatProperties: true,
|
||||
}
|
||||
)
|
||||
|
||||
// The geojson contains _all_ features at the given location
|
||||
// We only keep what is needed
|
||||
|
||||
geojson.features = geojson.features.filter(feature => this.allowedTags.matchesProperties(feature.properties))
|
||||
geojson.features = geojson.features.filter((feature) =>
|
||||
this.allowedTags.matchesProperties(feature.properties)
|
||||
)
|
||||
|
||||
for (let i = 0; i < geojson.features.length; i++) {
|
||||
geojson.features[i] = await this.patchIncompleteRelations(geojson.features[i], osmJson)
|
||||
geojson.features[i] = await this.patchIncompleteRelations(
|
||||
geojson.features[i],
|
||||
osmJson
|
||||
)
|
||||
}
|
||||
geojson.features.forEach(f => {
|
||||
geojson.features.forEach((f) => {
|
||||
f.properties["_backend"] = this._backend
|
||||
})
|
||||
|
||||
const index = Tiles.tile_index(z, x, y);
|
||||
new PerLayerFeatureSourceSplitter(this.filteredLayers,
|
||||
const index = Tiles.tile_index(z, x, y)
|
||||
new PerLayerFeatureSourceSplitter(
|
||||
this.filteredLayers,
|
||||
this.handleTile,
|
||||
StaticFeatureSource.fromGeojson(geojson.features),
|
||||
{
|
||||
tileIndex: index
|
||||
tileIndex: index,
|
||||
}
|
||||
);
|
||||
)
|
||||
if (this.options.markTileVisited) {
|
||||
this.options.markTileVisited(index)
|
||||
}
|
||||
}catch(e){
|
||||
console.error("PANIC: got the tile from the OSM-api, but something crashed handling this tile")
|
||||
error = e;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"PANIC: got the tile from the OSM-api, but something crashed handling this tile"
|
||||
)
|
||||
error = e
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Could not download tile", z, x, y, "due to", e, "; retrying with smaller bounds")
|
||||
console.error(
|
||||
"Could not download tile",
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
"due to",
|
||||
e,
|
||||
"; retrying with smaller bounds"
|
||||
)
|
||||
if (e === "rate limited") {
|
||||
return;
|
||||
return
|
||||
}
|
||||
await this.LoadTile(z + 1, x * 2, y * 2)
|
||||
await this.LoadTile(z + 1, 1 + x * 2, y * 2)
|
||||
|
@ -183,10 +209,8 @@ export default class OsmFeatureSource {
|
|||
await this.LoadTile(z + 1, 1 + x * 2, 1 + y * 2)
|
||||
}
|
||||
|
||||
if(error !== undefined){
|
||||
throw error;
|
||||
if (error !== undefined) {
|
||||
throw error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
import FeatureSource, {Tiled} from "../FeatureSource";
|
||||
import {BBox} from "../../BBox";
|
||||
import FeatureSource, { Tiled } from "../FeatureSource"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
export default interface TileHierarchy<T extends FeatureSource & Tiled> {
|
||||
|
||||
/**
|
||||
* A mapping from 'tile_index' to the actual tile featrues
|
||||
*/
|
||||
loadedTiles: Map<number, T>
|
||||
|
||||
}
|
||||
|
||||
export class TileHierarchyTools {
|
||||
|
||||
public static getTiles<T extends FeatureSource & Tiled>(hierarchy: TileHierarchy<T>, bbox: BBox): T[] {
|
||||
public static getTiles<T extends FeatureSource & Tiled>(
|
||||
hierarchy: TileHierarchy<T>,
|
||||
bbox: BBox
|
||||
): T[] {
|
||||
const result: T[] = []
|
||||
hierarchy.loadedTiles.forEach((tile) => {
|
||||
if (tile.bbox.overlapsWith(bbox)) {
|
||||
result.push(tile)
|
||||
}
|
||||
})
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,32 @@
|
|||
import TileHierarchy from "./TileHierarchy";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import FeatureSourceMerger from "../Sources/FeatureSourceMerger";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
import TileHierarchy from "./TileHierarchy"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> {
|
||||
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
|
||||
public readonly layer: FilteredLayer;
|
||||
private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<number, UIEventSource<FeatureSource[]>>();
|
||||
private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void;
|
||||
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<
|
||||
number,
|
||||
FeatureSourceForLayer & Tiled
|
||||
>()
|
||||
public readonly layer: FilteredLayer
|
||||
private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<
|
||||
number,
|
||||
UIEventSource<FeatureSource[]>
|
||||
>()
|
||||
private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void
|
||||
|
||||
constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, index: number) => void) {
|
||||
this.layer = layer;
|
||||
this._handleTile = handleTile;
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
handleTile: (
|
||||
src: FeatureSourceForLayer & IndexedFeatureSource & Tiled,
|
||||
index: number
|
||||
) => void
|
||||
) {
|
||||
this.layer = layer
|
||||
this._handleTile = handleTile
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,22 +35,24 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer
|
|||
* @param src
|
||||
*/
|
||||
public registerTile(src: FeatureSource & Tiled) {
|
||||
|
||||
const index = src.tileIndex
|
||||
if (this.sources.has(index)) {
|
||||
const sources = this.sources.get(index)
|
||||
sources.data.push(src)
|
||||
sources.ping()
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// We have to setup
|
||||
const sources = new UIEventSource<FeatureSource[]>([src])
|
||||
this.sources.set(index, sources)
|
||||
const merger = new FeatureSourceMerger(this.layer, index, BBox.fromTile(...Tiles.tile_from_index(index)), sources)
|
||||
const merger = new FeatureSourceMerger(
|
||||
this.layer,
|
||||
index,
|
||||
BBox.fromTile(...Tiles.tile_from_index(index)),
|
||||
sources
|
||||
)
|
||||
this.loadedTiles.set(index, merger)
|
||||
this._handleTile(merger, index)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,53 +1,65 @@
|
|||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import {Store, UIEventSource} from "../../UIEventSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import TileHierarchy from "./TileHierarchy";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource"
|
||||
import { Store, UIEventSource } from "../../UIEventSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import TileHierarchy from "./TileHierarchy"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
/**
|
||||
* Contains all features in a tiled fashion.
|
||||
* The data will be automatically broken down into subtiles when there are too much features in a single tile or if the zoomlevel is too high
|
||||
*/
|
||||
export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, FeatureSourceForLayer, TileHierarchy<IndexedFeatureSource & FeatureSourceForLayer & Tiled> {
|
||||
public readonly z: number;
|
||||
public readonly x: number;
|
||||
public readonly y: number;
|
||||
public readonly parent: TiledFeatureSource;
|
||||
export default class TiledFeatureSource
|
||||
implements
|
||||
Tiled,
|
||||
IndexedFeatureSource,
|
||||
FeatureSourceForLayer,
|
||||
TileHierarchy<IndexedFeatureSource & FeatureSourceForLayer & Tiled>
|
||||
{
|
||||
public readonly z: number
|
||||
public readonly x: number
|
||||
public readonly y: number
|
||||
public readonly parent: TiledFeatureSource
|
||||
public readonly root: TiledFeatureSource
|
||||
public readonly layer: FilteredLayer;
|
||||
public readonly layer: FilteredLayer
|
||||
/* An index of all known tiles. allTiles[z][x][y].get('layerid') will yield the corresponding tile.
|
||||
* Only defined on the root element!
|
||||
* Only defined on the root element!
|
||||
*/
|
||||
public readonly loadedTiles: Map<number, TiledFeatureSource & FeatureSourceForLayer> = undefined;
|
||||
public readonly loadedTiles: Map<number, TiledFeatureSource & FeatureSourceForLayer> = undefined
|
||||
|
||||
public readonly maxFeatureCount: number;
|
||||
public readonly name;
|
||||
public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>
|
||||
public readonly maxFeatureCount: number
|
||||
public readonly name
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>
|
||||
public readonly containedIds: Store<Set<string>>
|
||||
|
||||
public readonly bbox: BBox;
|
||||
public readonly tileIndex: number;
|
||||
public readonly bbox: BBox
|
||||
public readonly tileIndex: number
|
||||
private upper_left: TiledFeatureSource
|
||||
private upper_right: TiledFeatureSource
|
||||
private lower_left: TiledFeatureSource
|
||||
private lower_right: TiledFeatureSource
|
||||
private readonly maxzoom: number;
|
||||
private readonly maxzoom: number
|
||||
private readonly options: TiledFeatureSourceOptions
|
||||
|
||||
private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) {
|
||||
this.z = z;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
private constructor(
|
||||
z: number,
|
||||
x: number,
|
||||
y: number,
|
||||
parent: TiledFeatureSource,
|
||||
options?: TiledFeatureSourceOptions
|
||||
) {
|
||||
this.z = z
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.bbox = BBox.fromTile(z, x, y)
|
||||
this.tileIndex = Tiles.tile_index(z, x, y)
|
||||
this.name = `TiledFeatureSource(${z},${x},${y})`
|
||||
this.parent = parent;
|
||||
this.parent = parent
|
||||
this.layer = options.layer
|
||||
options = options ?? {}
|
||||
this.maxFeatureCount = options?.maxFeatureCount ?? 250;
|
||||
this.maxFeatureCount = options?.maxFeatureCount ?? 250
|
||||
this.maxzoom = options.maxZoomLevel ?? 18
|
||||
this.options = options;
|
||||
this.options = options
|
||||
if (parent === undefined) {
|
||||
throw "Parent is not allowed to be undefined. Use null instead"
|
||||
}
|
||||
|
@ -55,50 +67,51 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
throw "Invalid root tile: z, x and y should all be null"
|
||||
}
|
||||
if (parent === null) {
|
||||
this.root = this;
|
||||
this.root = this
|
||||
this.loadedTiles = new Map()
|
||||
} else {
|
||||
this.root = this.parent.root;
|
||||
this.loadedTiles = this.root.loadedTiles;
|
||||
this.root = this.parent.root
|
||||
this.loadedTiles = this.root.loadedTiles
|
||||
const i = Tiles.tile_index(z, x, y)
|
||||
this.root.loadedTiles.set(i, this)
|
||||
}
|
||||
this.features = new UIEventSource<any[]>([])
|
||||
this.containedIds = this.features.map(features => {
|
||||
this.containedIds = this.features.map((features) => {
|
||||
if (features === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return new Set(features.map(f => f.feature.properties.id))
|
||||
return new Set(features.map((f) => f.feature.properties.id))
|
||||
})
|
||||
|
||||
// We register this tile, but only when there is some data in it
|
||||
if (this.options.registerTile !== undefined) {
|
||||
this.features.addCallbackAndRunD(features => {
|
||||
this.features.addCallbackAndRunD((features) => {
|
||||
if (features.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this.options.registerTile(this)
|
||||
return true;
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static createHierarchy(features: FeatureSource, options?: TiledFeatureSourceOptions): TiledFeatureSource {
|
||||
public static createHierarchy(
|
||||
features: FeatureSource,
|
||||
options?: TiledFeatureSourceOptions
|
||||
): TiledFeatureSource {
|
||||
options = {
|
||||
...options,
|
||||
layer: features["layer"] ?? options.layer
|
||||
layer: features["layer"] ?? options.layer,
|
||||
}
|
||||
const root = new TiledFeatureSource(0, 0, 0, null, options)
|
||||
features.features?.addCallbackAndRunD(feats => root.addFeatures(feats))
|
||||
return root;
|
||||
features.features?.addCallbackAndRunD((feats) => root.addFeatures(feats))
|
||||
return root
|
||||
}
|
||||
|
||||
private isSplitNeeded(featureCount: number) {
|
||||
if (this.upper_left !== undefined) {
|
||||
// This tile has been split previously, so we keep on splitting
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
if (this.z >= this.maxzoom) {
|
||||
// We are not allowed to split any further
|
||||
|
@ -111,7 +124,6 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
|
||||
// To much features - we split
|
||||
return featureCount > this.maxFeatureCount
|
||||
|
||||
}
|
||||
|
||||
/***
|
||||
|
@ -120,21 +132,45 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
* @param features
|
||||
* @private
|
||||
*/
|
||||
private addFeatures(features: { feature: any, freshness: Date }[]) {
|
||||
private addFeatures(features: { feature: any; freshness: Date }[]) {
|
||||
if (features === undefined || features.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.isSplitNeeded(features.length)) {
|
||||
this.features.setData(features)
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (this.upper_left === undefined) {
|
||||
this.upper_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2, this, this.options)
|
||||
this.upper_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2, this, this.options)
|
||||
this.lower_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2 + 1, this, this.options)
|
||||
this.lower_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2 + 1, this, this.options)
|
||||
this.upper_left = new TiledFeatureSource(
|
||||
this.z + 1,
|
||||
this.x * 2,
|
||||
this.y * 2,
|
||||
this,
|
||||
this.options
|
||||
)
|
||||
this.upper_right = new TiledFeatureSource(
|
||||
this.z + 1,
|
||||
this.x * 2 + 1,
|
||||
this.y * 2,
|
||||
this,
|
||||
this.options
|
||||
)
|
||||
this.lower_left = new TiledFeatureSource(
|
||||
this.z + 1,
|
||||
this.x * 2,
|
||||
this.y * 2 + 1,
|
||||
this,
|
||||
this.options
|
||||
)
|
||||
this.lower_right = new TiledFeatureSource(
|
||||
this.z + 1,
|
||||
this.x * 2 + 1,
|
||||
this.y * 2 + 1,
|
||||
this,
|
||||
this.options
|
||||
)
|
||||
}
|
||||
|
||||
const ulf = []
|
||||
|
@ -147,7 +183,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
const bbox = BBox.get(feature.feature)
|
||||
|
||||
// There are a few strategies to deal with features that cross tile boundaries
|
||||
|
||||
|
||||
if (this.options.noDuplicates) {
|
||||
// Strategy 1: We put the feature into a somewhat matching tile
|
||||
if (bbox.overlapsWith(this.upper_left.bbox)) {
|
||||
|
@ -195,19 +231,18 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
this.lower_left.addFeatures(llf)
|
||||
this.lower_right.addFeatures(lrf)
|
||||
this.features.setData(overlapsboundary)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export interface TiledFeatureSourceOptions {
|
||||
readonly maxFeatureCount?: number,
|
||||
readonly maxZoomLevel?: number,
|
||||
readonly minZoomLevel?: number,
|
||||
readonly maxFeatureCount?: number
|
||||
readonly maxZoomLevel?: number
|
||||
readonly minZoomLevel?: number
|
||||
/**
|
||||
* IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated.
|
||||
* Setting 'dontEnforceMinZoomLevel' will assign to feature to some matching subtile.
|
||||
*/
|
||||
readonly noDuplicates?: boolean,
|
||||
readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void,
|
||||
readonly noDuplicates?: boolean
|
||||
readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void
|
||||
readonly layer?: FilteredLayer
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
import * as turf from '@turf/turf'
|
||||
import {BBox} from "./BBox";
|
||||
import * as turf from "@turf/turf"
|
||||
import { BBox } from "./BBox"
|
||||
import togpx from "togpx"
|
||||
import Constants from "../Models/Constants";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import {AllGeoJSON, booleanWithin, Coord, Feature, Geometry, MultiPolygon, Polygon, Properties} from "@turf/turf";
|
||||
import Constants from "../Models/Constants"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import {
|
||||
AllGeoJSON,
|
||||
booleanWithin,
|
||||
Coord,
|
||||
Feature,
|
||||
Geometry,
|
||||
MultiPolygon,
|
||||
Polygon,
|
||||
Properties,
|
||||
} from "@turf/turf"
|
||||
|
||||
export class GeoOperations {
|
||||
|
||||
private static readonly _earthRadius = 6378137;
|
||||
private static readonly _originShift = 2 * Math.PI * GeoOperations._earthRadius / 2;
|
||||
private static readonly _earthRadius = 6378137
|
||||
private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2
|
||||
|
||||
static surfaceAreaInSqMeters(feature: any) {
|
||||
return turf.area(feature);
|
||||
return turf.area(feature)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -19,10 +27,10 @@ export class GeoOperations {
|
|||
* @param feature
|
||||
*/
|
||||
static centerpoint(feature: any) {
|
||||
const newFeature = turf.center(feature);
|
||||
newFeature.properties = feature.properties;
|
||||
newFeature.id = feature.id;
|
||||
return newFeature;
|
||||
const newFeature = turf.center(feature)
|
||||
newFeature.properties = feature.properties
|
||||
newFeature.id = feature.id
|
||||
return newFeature
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,7 +38,7 @@ export class GeoOperations {
|
|||
* @param feature
|
||||
*/
|
||||
static centerpointCoordinates(feature: AllGeoJSON): [number, number] {
|
||||
return <[number, number]>turf.center(feature).geometry.coordinates;
|
||||
return <[number, number]>turf.center(feature).geometry.coordinates
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,7 +47,7 @@ export class GeoOperations {
|
|||
* @param lonlat1
|
||||
*/
|
||||
static distanceBetween(lonlat0: [number, number], lonlat1: [number, number]) {
|
||||
return turf.distance(lonlat0, lonlat1, {units: "meters"})
|
||||
return turf.distance(lonlat0, lonlat1, { units: "meters" })
|
||||
}
|
||||
|
||||
static convexHull(featureCollection, options: { concavity?: number }) {
|
||||
|
@ -69,16 +77,17 @@ export class GeoOperations {
|
|||
* const overlap0 = GeoOperations.calculateOverlap(line0, [polygon]);
|
||||
* overlap.length // => 1
|
||||
*/
|
||||
static calculateOverlap(feature: any, otherFeatures: any[]): { feat: any, overlap: number }[] {
|
||||
|
||||
const featureBBox = BBox.get(feature);
|
||||
const result: { feat: any, overlap: number }[] = [];
|
||||
static calculateOverlap(feature: any, otherFeatures: any[]): { feat: any; overlap: number }[] {
|
||||
const featureBBox = BBox.get(feature)
|
||||
const result: { feat: any; overlap: number }[] = []
|
||||
if (feature.geometry.type === "Point") {
|
||||
const coor = feature.geometry.coordinates;
|
||||
const coor = feature.geometry.coordinates
|
||||
for (const otherFeature of otherFeatures) {
|
||||
|
||||
if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) {
|
||||
continue;
|
||||
if (
|
||||
feature.properties.id !== undefined &&
|
||||
feature.properties.id === otherFeature.properties.id
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (otherFeature.geometry === undefined) {
|
||||
|
@ -87,86 +96,105 @@ export class GeoOperations {
|
|||
}
|
||||
|
||||
if (GeoOperations.inside(coor, otherFeature)) {
|
||||
result.push({feat: otherFeature, overlap: undefined})
|
||||
result.push({ feat: otherFeature, overlap: undefined })
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
if (feature.geometry.type === "LineString") {
|
||||
|
||||
for (const otherFeature of otherFeatures) {
|
||||
|
||||
if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) {
|
||||
continue;
|
||||
if (
|
||||
feature.properties.id !== undefined &&
|
||||
feature.properties.id === otherFeature.properties.id
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const intersection = GeoOperations.calculateInstersection(feature, otherFeature, featureBBox)
|
||||
const intersection = GeoOperations.calculateInstersection(
|
||||
feature,
|
||||
otherFeature,
|
||||
featureBBox
|
||||
)
|
||||
if (intersection === null) {
|
||||
continue
|
||||
}
|
||||
result.push({feat: otherFeature, overlap: intersection})
|
||||
|
||||
result.push({ feat: otherFeature, overlap: intersection })
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") {
|
||||
|
||||
for (const otherFeature of otherFeatures) {
|
||||
|
||||
if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) {
|
||||
continue;
|
||||
if (
|
||||
feature.properties.id !== undefined &&
|
||||
feature.properties.id === otherFeature.properties.id
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (otherFeature.geometry.type === "Point") {
|
||||
if (this.inside(otherFeature, feature)) {
|
||||
result.push({feat: otherFeature, overlap: undefined})
|
||||
result.push({ feat: otherFeature, overlap: undefined })
|
||||
}
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// Calculate the surface area of the intersection
|
||||
|
||||
const intersection = this.calculateInstersection(feature, otherFeature, featureBBox)
|
||||
if (intersection === null) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
result.push({feat: otherFeature, overlap: intersection})
|
||||
|
||||
result.push({ feat: otherFeature, overlap: intersection })
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
console.error("Could not correctly calculate the overlap of ", feature, ": unsupported type")
|
||||
return result;
|
||||
console.error(
|
||||
"Could not correctly calculate the overlap of ",
|
||||
feature,
|
||||
": unsupported type"
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function which does the heavy lifting for 'inside'
|
||||
*/
|
||||
private static pointInPolygonCoordinates(x: number, y: number, coordinates: [number, number][][]) {
|
||||
const inside = GeoOperations.pointWithinRing(x, y, /*This is the outer ring of the polygon */coordinates[0])
|
||||
private static pointInPolygonCoordinates(
|
||||
x: number,
|
||||
y: number,
|
||||
coordinates: [number, number][][]
|
||||
) {
|
||||
const inside = GeoOperations.pointWithinRing(
|
||||
x,
|
||||
y,
|
||||
/*This is the outer ring of the polygon */ coordinates[0]
|
||||
)
|
||||
if (!inside) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
for (let i = 1; i < coordinates.length; i++) {
|
||||
const inHole = GeoOperations.pointWithinRing(x, y, coordinates[i] /* These are inner rings, aka holes*/)
|
||||
const inHole = GeoOperations.pointWithinRing(
|
||||
x,
|
||||
y,
|
||||
coordinates[i] /* These are inner rings, aka holes*/
|
||||
)
|
||||
if (inHole) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect wether or not the given point is located in the feature
|
||||
*
|
||||
*
|
||||
* // Should work with a normal polygon
|
||||
* const polygon = {"type": "Feature","properties": {},"geometry": {"type": "Polygon","coordinates": [[[1.8017578124999998,50.401515322782366],[-3.1640625,46.255846818480315],[5.185546875,44.74673324024678],[1.8017578124999998,50.401515322782366]]]}};
|
||||
* GeoOperations.inside([3.779296875, 48.777912755501845], polygon) // => false
|
||||
* GeoOperations.inside([1.23046875, 47.60616304386874], polygon) // => true
|
||||
*
|
||||
*
|
||||
* // should work with a multipolygon and detect holes
|
||||
* const multiPolygon = {"type": "Feature", "properties": {},
|
||||
* "geometry": {
|
||||
|
@ -186,37 +214,32 @@ export class GeoOperations {
|
|||
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
|
||||
|
||||
if (feature.geometry.type === "Point") {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
if (pointCoordinate.geometry !== undefined) {
|
||||
pointCoordinate = pointCoordinate.geometry.coordinates
|
||||
}
|
||||
|
||||
const x: number = pointCoordinate[0];
|
||||
const y: number = pointCoordinate[1];
|
||||
|
||||
const x: number = pointCoordinate[0]
|
||||
const y: number = pointCoordinate[1]
|
||||
|
||||
if (feature.geometry.type === "MultiPolygon") {
|
||||
const coordinatess = feature.geometry.coordinates;
|
||||
const coordinatess = feature.geometry.coordinates
|
||||
for (const coordinates of coordinatess) {
|
||||
const inThisPolygon = GeoOperations.pointInPolygonCoordinates(x, y, coordinates)
|
||||
if (inThisPolygon) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (feature.geometry.type === "Polygon") {
|
||||
return GeoOperations.pointInPolygonCoordinates(x, y, feature.geometry.coordinates)
|
||||
}
|
||||
|
||||
throw "GeoOperations.inside: unsupported geometry type "+feature.geometry.type
|
||||
|
||||
|
||||
throw "GeoOperations.inside: unsupported geometry type " + feature.geometry.type
|
||||
}
|
||||
|
||||
static lengthInMeters(feature: any) {
|
||||
|
@ -225,39 +248,24 @@ export class GeoOperations {
|
|||
|
||||
static buffer(feature: any, bufferSizeInMeter: number) {
|
||||
return turf.buffer(feature, bufferSizeInMeter / 1000, {
|
||||
units: 'kilometers'
|
||||
units: "kilometers",
|
||||
})
|
||||
}
|
||||
|
||||
static bbox(feature: any) {
|
||||
const [lon, lat, lon0, lat0] = turf.bbox(feature)
|
||||
return {
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[
|
||||
lon,
|
||||
lat
|
||||
],
|
||||
[
|
||||
lon0,
|
||||
lat
|
||||
],
|
||||
[
|
||||
lon0,
|
||||
lat0
|
||||
],
|
||||
[
|
||||
lon,
|
||||
lat0
|
||||
],
|
||||
[
|
||||
lon,
|
||||
lat
|
||||
],
|
||||
]
|
||||
}
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
[lon, lat],
|
||||
[lon0, lat],
|
||||
[lon0, lat0],
|
||||
[lon, lat0],
|
||||
[lon, lat],
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,18 +281,17 @@ export class GeoOperations {
|
|||
*/
|
||||
public static nearestPoint(way, point: [number, number]) {
|
||||
if (way.geometry.type === "Polygon") {
|
||||
way = {...way}
|
||||
way.geometry = {...way.geometry}
|
||||
way = { ...way }
|
||||
way.geometry = { ...way.geometry }
|
||||
way.geometry.type = "LineString"
|
||||
way.geometry.coordinates = way.geometry.coordinates[0]
|
||||
}
|
||||
|
||||
return turf.nearestPointOnLine(way, point, {units: "kilometers"});
|
||||
return turf.nearestPointOnLine(way, point, { units: "kilometers" })
|
||||
}
|
||||
|
||||
public static toCSV(features: any[]): string {
|
||||
|
||||
const headerValuesSeen = new Set<string>();
|
||||
const headerValuesSeen = new Set<string>()
|
||||
const headerValuesOrdered: string[] = []
|
||||
|
||||
function addH(key) {
|
||||
|
@ -300,18 +307,17 @@ export class GeoOperations {
|
|||
const lines: string[] = []
|
||||
|
||||
for (const feature of features) {
|
||||
const properties = feature.properties;
|
||||
const properties = feature.properties
|
||||
for (const key in properties) {
|
||||
if (!properties.hasOwnProperty(key)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
addH(key)
|
||||
|
||||
}
|
||||
}
|
||||
headerValuesOrdered.sort()
|
||||
for (const feature of features) {
|
||||
const properties = feature.properties;
|
||||
const properties = feature.properties
|
||||
let line = ""
|
||||
for (const key of headerValuesOrdered) {
|
||||
const value = properties[key]
|
||||
|
@ -324,27 +330,27 @@ export class GeoOperations {
|
|||
lines.push(line)
|
||||
}
|
||||
|
||||
return headerValuesOrdered.map(v => JSON.stringify(v)).join(",") + "\n" + lines.join("\n")
|
||||
return headerValuesOrdered.map((v) => JSON.stringify(v)).join(",") + "\n" + lines.join("\n")
|
||||
}
|
||||
|
||||
//Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913
|
||||
public static ConvertWgs84To900913(lonLat: [number, number]): [number, number] {
|
||||
const lon = lonLat[0];
|
||||
const lat = lonLat[1];
|
||||
const x = lon * GeoOperations._originShift / 180;
|
||||
let y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
|
||||
y = y * GeoOperations._originShift / 180;
|
||||
return [x, y];
|
||||
const lon = lonLat[0]
|
||||
const lat = lonLat[1]
|
||||
const x = (lon * GeoOperations._originShift) / 180
|
||||
let y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360)) / (Math.PI / 180)
|
||||
y = (y * GeoOperations._originShift) / 180
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
//Converts XY point from (Spherical) Web Mercator EPSG:3785 (unofficially EPSG:900913) to lat/lon in WGS84 Datum
|
||||
public static Convert900913ToWgs84(lonLat: [number, number]): [number, number] {
|
||||
const lon = lonLat[0]
|
||||
const lat = lonLat[1]
|
||||
const x = 180 * lon / GeoOperations._originShift;
|
||||
let y = 180 * lat / GeoOperations._originShift;
|
||||
y = 180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2);
|
||||
return [x, y];
|
||||
const x = (180 * lon) / GeoOperations._originShift
|
||||
let y = (180 * lat) / GeoOperations._originShift
|
||||
y = (180 / Math.PI) * (2 * Math.atan(Math.exp((y * Math.PI) / 180)) - Math.PI / 2)
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
public static GeoJsonToWGS84(geojson) {
|
||||
|
@ -360,10 +366,10 @@ export class GeoOperations {
|
|||
public static SimplifyCoordinates(coordinates: [number, number][]) {
|
||||
const newCoordinates = []
|
||||
for (let i = 1; i < coordinates.length - 1; i++) {
|
||||
const coordinate = coordinates[i];
|
||||
const coordinate = coordinates[i]
|
||||
const prev = coordinates[i - 1]
|
||||
const next = coordinates[i + 1]
|
||||
const b0 = turf.bearing(prev, coordinate, {final: true})
|
||||
const b0 = turf.bearing(prev, coordinate, { final: true })
|
||||
const b1 = turf.bearing(coordinate, next)
|
||||
|
||||
const diff = Math.abs(b1 - b0)
|
||||
|
@ -373,27 +379,27 @@ export class GeoOperations {
|
|||
newCoordinates.push(coordinate)
|
||||
}
|
||||
return newCoordinates
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates line intersection between two features.
|
||||
*/
|
||||
public static LineIntersections(feature, otherFeature): [number, number][] {
|
||||
return turf.lineIntersect(feature, otherFeature).features.map(p => <[number, number]>p.geometry.coordinates)
|
||||
return turf
|
||||
.lineIntersect(feature, otherFeature)
|
||||
.features.map((p) => <[number, number]>p.geometry.coordinates)
|
||||
}
|
||||
|
||||
public static AsGpx(feature, generatedWithLayer?: LayerConfig) {
|
||||
|
||||
const metadata = {}
|
||||
const tags = feature.properties
|
||||
|
||||
if (generatedWithLayer !== undefined) {
|
||||
|
||||
metadata["name"] = generatedWithLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt
|
||||
metadata["desc"] = "Generated with MapComplete layer " + generatedWithLayer.id
|
||||
if (tags._backend?.contains("openstreetmap")) {
|
||||
metadata["copyright"] = "Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright"
|
||||
metadata["copyright"] =
|
||||
"Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright"
|
||||
metadata["author"] = tags["_last_edit:contributor"]
|
||||
metadata["link"] = "https://www.openstreetmap.org/" + tags.id
|
||||
metadata["time"] = tags["_last_edit:timestamp"]
|
||||
|
@ -404,18 +410,22 @@ export class GeoOperations {
|
|||
|
||||
return togpx(feature, {
|
||||
creator: "MapComplete " + Constants.vNumber,
|
||||
metadata
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
public static IdentifieCommonSegments(coordinatess: [number, number][][]): {
|
||||
originalIndex: number,
|
||||
segmentShardWith: number[],
|
||||
originalIndex: number
|
||||
segmentShardWith: number[]
|
||||
coordinates: []
|
||||
}[] {
|
||||
|
||||
// An edge. Note that the edge might be reversed to fix the sorting condition: start[0] < end[0] && (start[0] != end[0] || start[0] < end[1])
|
||||
type edge = { start: [number, number], end: [number, number], intermediate: [number, number][], members: { index: number, isReversed: boolean }[] }
|
||||
type edge = {
|
||||
start: [number, number]
|
||||
end: [number, number]
|
||||
intermediate: [number, number][]
|
||||
members: { index: number; isReversed: boolean }[]
|
||||
}
|
||||
|
||||
// The strategy:
|
||||
// 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them
|
||||
|
@ -425,12 +435,11 @@ export class GeoOperations {
|
|||
const allEdgesByKey = new Map<string, edge>()
|
||||
|
||||
for (let index = 0; index < coordinatess.length; index++) {
|
||||
const coordinates = coordinatess[index];
|
||||
const coordinates = coordinatess[index]
|
||||
for (let i = 0; i < coordinates.length - 1; i++) {
|
||||
|
||||
const c0 = coordinates[i];
|
||||
const c0 = coordinates[i]
|
||||
const c1 = coordinates[i + 1]
|
||||
const isReversed = (c0[0] > c1[0]) || (c0[0] == c1[0] && c0[1] > c1[1])
|
||||
const isReversed = c0[0] > c1[0] || (c0[0] == c1[0] && c0[1] > c1[1])
|
||||
|
||||
let key: string
|
||||
if (isReversed) {
|
||||
|
@ -438,40 +447,38 @@ export class GeoOperations {
|
|||
} else {
|
||||
key = "" + c0 + ";" + c1
|
||||
}
|
||||
const member = {index, isReversed}
|
||||
const member = { index, isReversed }
|
||||
if (allEdgesByKey.has(key)) {
|
||||
allEdgesByKey.get(key).members.push(member)
|
||||
continue
|
||||
}
|
||||
|
||||
let edge: edge;
|
||||
let edge: edge
|
||||
if (!isReversed) {
|
||||
edge = {
|
||||
start: c0,
|
||||
end: c1,
|
||||
members: [member],
|
||||
intermediate: []
|
||||
intermediate: [],
|
||||
}
|
||||
} else {
|
||||
edge = {
|
||||
start: c1,
|
||||
end: c0,
|
||||
members: [member],
|
||||
intermediate: []
|
||||
intermediate: [],
|
||||
}
|
||||
}
|
||||
allEdgesByKey.set(key, edge)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Lets merge them back together!
|
||||
|
||||
let didMergeSomething = false;
|
||||
let didMergeSomething = false
|
||||
let allMergedEdges = Array.from(allEdgesByKey.values())
|
||||
const allEdgesByStartPoint = new Map<string, edge[]>()
|
||||
for (const edge of allMergedEdges) {
|
||||
|
||||
edge.members.sort((m0, m1) => m0.index - m1.index)
|
||||
|
||||
const kstart = edge.start + ""
|
||||
|
@ -481,7 +488,6 @@ export class GeoOperations {
|
|||
allEdgesByStartPoint.get(kstart).push(edge)
|
||||
}
|
||||
|
||||
|
||||
function membersAreCompatible(first: edge, second: edge): boolean {
|
||||
// There must be an exact match between the members
|
||||
if (first.members === second.members) {
|
||||
|
@ -504,7 +510,6 @@ export class GeoOperations {
|
|||
// Allrigth, they are the same, lets mark this permanently
|
||||
second.members = first.members
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
do {
|
||||
|
@ -524,9 +529,8 @@ export class GeoOperations {
|
|||
continue
|
||||
}
|
||||
|
||||
|
||||
for (let i = 0; i < matchingEndEdges.length; i++) {
|
||||
const endEdge = matchingEndEdges[i];
|
||||
const endEdge = matchingEndEdges[i]
|
||||
|
||||
if (consumed.has(endEdge)) {
|
||||
continue
|
||||
|
@ -543,12 +547,11 @@ export class GeoOperations {
|
|||
edge.end = endEdge.end
|
||||
consumed.add(endEdge)
|
||||
matchingEndEdges.splice(i, 1)
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
allMergedEdges = allMergedEdges.filter(edge => !consumed.has(edge));
|
||||
|
||||
allMergedEdges = allMergedEdges.filter((edge) => !consumed.has(edge))
|
||||
} while (didMergeSomething)
|
||||
|
||||
return []
|
||||
|
@ -557,7 +560,7 @@ export class GeoOperations {
|
|||
/**
|
||||
* Removes points that do not contribute to the geometry from linestrings and the outer ring of polygons.
|
||||
* Returs a new copy of the feature
|
||||
*
|
||||
*
|
||||
* const feature = {"geometry": {"type": "Polygon","coordinates": [[[4.477944199999975,51.02783550000022],[4.477987899999996,51.027818800000034],[4.478004500000021,51.02783399999988],[4.478025499999962,51.02782489999994],[4.478079099999993,51.027873899999896],[4.47801040000006,51.027903799999955],[4.477964799999972,51.02785709999982],[4.477964699999964,51.02785690000006],[4.477944199999975,51.02783550000022]]]}}
|
||||
* const copy = GeoOperations.removeOvernoding(feature)
|
||||
* expect(copy.geometry.coordinates[0]).deep.equal([[4.477944199999975,51.02783550000022],[4.477987899999996,51.027818800000034],[4.478004500000021,51.02783399999988],[4.478025499999962,51.02782489999994],[4.478079099999993,51.027873899999896],[4.47801040000006,51.027903799999955],[4.477944199999975,51.02783550000022]])
|
||||
|
@ -569,7 +572,7 @@ export class GeoOperations {
|
|||
|
||||
const copy = {
|
||||
...feature,
|
||||
geometry: {...feature.geometry}
|
||||
geometry: { ...feature.geometry },
|
||||
}
|
||||
let coordinates: [number, number][]
|
||||
if (feature.geometry.type === "LineString") {
|
||||
|
@ -582,7 +585,7 @@ export class GeoOperations {
|
|||
|
||||
// inline replacement in the coordinates list
|
||||
for (let i = coordinates.length - 2; i >= 1; i--) {
|
||||
const coordinate = coordinates[i];
|
||||
const coordinate = coordinates[i]
|
||||
const nextCoordinate = coordinates[i + 1]
|
||||
const prevCoordinate = coordinates[i - 1]
|
||||
|
||||
|
@ -610,30 +613,27 @@ export class GeoOperations {
|
|||
// In case that the line is going south, e.g. bearingN = 179, bearingP = -179
|
||||
coordinates.splice(i, 1)
|
||||
}
|
||||
|
||||
}
|
||||
return copy;
|
||||
|
||||
return copy
|
||||
}
|
||||
|
||||
private static pointWithinRing(x: number, y: number, ring: [number, number][]) {
|
||||
let inside = false;
|
||||
let inside = false
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
||||
const coori = ring[i];
|
||||
const coorj = ring[j];
|
||||
const coori = ring[i]
|
||||
const coorj = ring[j]
|
||||
|
||||
const xi = coori[0];
|
||||
const yi = coori[1];
|
||||
const xj = coorj[0];
|
||||
const yj = coorj[1];
|
||||
const xi = coori[0]
|
||||
const yi = coori[1]
|
||||
const xj = coorj[0]
|
||||
const yj = coorj[1]
|
||||
|
||||
const intersect = ((yi > y) != (yj > y))
|
||||
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
|
||||
const intersect = yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
|
||||
if (intersect) {
|
||||
inside = !inside;
|
||||
inside = !inside
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
return inside
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -642,46 +642,47 @@ export class GeoOperations {
|
|||
* Returns 0 if both are linestrings
|
||||
* Returns null if the features are not intersecting
|
||||
*/
|
||||
private static calculateInstersection(feature, otherFeature, featureBBox: BBox, otherFeatureBBox?: BBox): number {
|
||||
private static calculateInstersection(
|
||||
feature,
|
||||
otherFeature,
|
||||
featureBBox: BBox,
|
||||
otherFeatureBBox?: BBox
|
||||
): number {
|
||||
if (feature.geometry.type === "LineString") {
|
||||
|
||||
|
||||
otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature);
|
||||
otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature)
|
||||
const overlaps = featureBBox.overlapsWith(otherFeatureBBox)
|
||||
if (!overlaps) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Calculate the length of the intersection
|
||||
|
||||
|
||||
let intersectionPoints = turf.lineIntersect(feature, otherFeature);
|
||||
let intersectionPoints = turf.lineIntersect(feature, otherFeature)
|
||||
if (intersectionPoints.features.length == 0) {
|
||||
// No intersections.
|
||||
// If one point is inside of the polygon, all points are
|
||||
|
||||
|
||||
const coors = feature.geometry.coordinates;
|
||||
const coors = feature.geometry.coordinates
|
||||
const startCoor = coors[0]
|
||||
if (this.inside(startCoor, otherFeature)) {
|
||||
return this.lengthInMeters(feature)
|
||||
}
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let intersectionPointsArray = intersectionPoints.features.map(d => {
|
||||
let intersectionPointsArray = intersectionPoints.features.map((d) => {
|
||||
return d.geometry.coordinates
|
||||
});
|
||||
})
|
||||
|
||||
if (otherFeature.geometry.type === "LineString") {
|
||||
if (intersectionPointsArray.length > 0) {
|
||||
return 0
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
if (intersectionPointsArray.length == 1) {
|
||||
// We need to add the start- or endpoint of the current feature, depending on which one is embedded
|
||||
const coors = feature.geometry.coordinates;
|
||||
const coors = feature.geometry.coordinates
|
||||
const startCoor = coors[0]
|
||||
if (this.inside(startCoor, otherFeature)) {
|
||||
// The startpoint is embedded
|
||||
|
@ -691,46 +692,50 @@ export class GeoOperations {
|
|||
}
|
||||
}
|
||||
|
||||
let intersection = turf.lineSlice(turf.point(intersectionPointsArray[0]), turf.point(intersectionPointsArray[1]), feature);
|
||||
let intersection = turf.lineSlice(
|
||||
turf.point(intersectionPointsArray[0]),
|
||||
turf.point(intersectionPointsArray[1]),
|
||||
feature
|
||||
)
|
||||
|
||||
if (intersection == null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const intersectionSize = turf.length(intersection); // in km
|
||||
const intersectionSize = turf.length(intersection) // in km
|
||||
return intersectionSize * 1000
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") {
|
||||
const otherFeatureBBox = BBox.get(otherFeature);
|
||||
const otherFeatureBBox = BBox.get(otherFeature)
|
||||
const overlaps = featureBBox.overlapsWith(otherFeatureBBox)
|
||||
if (!overlaps) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
if (otherFeature.geometry.type === "LineString") {
|
||||
return this.calculateInstersection(otherFeature, feature, otherFeatureBBox, featureBBox)
|
||||
return this.calculateInstersection(
|
||||
otherFeature,
|
||||
feature,
|
||||
otherFeatureBBox,
|
||||
featureBBox
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const intersection = turf.intersect(feature, otherFeature);
|
||||
const intersection = turf.intersect(feature, otherFeature)
|
||||
if (intersection == null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return turf.area(intersection); // in m²
|
||||
return turf.area(intersection) // in m²
|
||||
} catch (e) {
|
||||
if (e.message === "Each LinearRing of a Polygon must have 4 or more Positions.") {
|
||||
// WORKAROUND TIME!
|
||||
// See https://github.com/Turfjs/turf/pull/2238
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
throw e;
|
||||
throw e
|
||||
}
|
||||
|
||||
}
|
||||
throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -742,7 +747,7 @@ export class GeoOperations {
|
|||
|
||||
/**
|
||||
* Returns 'true' if one feature contains the other feature
|
||||
*
|
||||
*
|
||||
* const pond: Feature<Polygon, any> = {
|
||||
* "type": "Feature",
|
||||
* "properties": {"natural":"water","water":"pond"},
|
||||
|
@ -769,9 +774,10 @@ export class GeoOperations {
|
|||
* GeoOperations.completelyWithin(pond, park) // => true
|
||||
* GeoOperations.completelyWithin(park, pond) // => false
|
||||
*/
|
||||
static completelyWithin(feature: Feature<Geometry, any>, possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any>) : boolean {
|
||||
return booleanWithin(feature, possiblyEncloingFeature);
|
||||
static completelyWithin(
|
||||
feature: Feature<Geometry, any>,
|
||||
possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any>
|
||||
): boolean {
|
||||
return booleanWithin(feature, possiblyEncloingFeature)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,45 +1,52 @@
|
|||
import {Mapillary} from "./Mapillary";
|
||||
import {WikimediaImageProvider} from "./WikimediaImageProvider";
|
||||
import {Imgur} from "./Imgur";
|
||||
import GenericImageProvider from "./GenericImageProvider";
|
||||
import {Store, UIEventSource} from "../UIEventSource";
|
||||
import ImageProvider, {ProvidedImage} from "./ImageProvider";
|
||||
import {WikidataImageProvider} from "./WikidataImageProvider";
|
||||
import { Mapillary } from "./Mapillary"
|
||||
import { WikimediaImageProvider } from "./WikimediaImageProvider"
|
||||
import { Imgur } from "./Imgur"
|
||||
import GenericImageProvider from "./GenericImageProvider"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||
import { WikidataImageProvider } from "./WikidataImageProvider"
|
||||
|
||||
/**
|
||||
* A generic 'from the interwebz' image picker, without attribution
|
||||
*/
|
||||
export default class AllImageProviders {
|
||||
|
||||
public static ImageAttributionSource: ImageProvider[] = [
|
||||
Imgur.singleton,
|
||||
Mapillary.singleton,
|
||||
WikidataImageProvider.singleton,
|
||||
WikimediaImageProvider.singleton,
|
||||
new GenericImageProvider(
|
||||
[].concat(...Imgur.defaultValuePrefix, ...WikimediaImageProvider.commonsPrefixes, ...Mapillary.valuePrefixes)
|
||||
)
|
||||
[].concat(
|
||||
...Imgur.defaultValuePrefix,
|
||||
...WikimediaImageProvider.commonsPrefixes,
|
||||
...Mapillary.valuePrefixes
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
private static providersByName= {
|
||||
"imgur": Imgur.singleton,
|
||||
"mapillary": Mapillary.singleton,
|
||||
"wikidata": WikidataImageProvider.singleton,
|
||||
"wikimedia": WikimediaImageProvider.singleton
|
||||
private static providersByName = {
|
||||
imgur: Imgur.singleton,
|
||||
mapillary: Mapillary.singleton,
|
||||
wikidata: WikidataImageProvider.singleton,
|
||||
wikimedia: WikimediaImageProvider.singleton,
|
||||
}
|
||||
|
||||
public static byName(name: string){
|
||||
|
||||
public static byName(name: string) {
|
||||
return AllImageProviders.providersByName[name.toLowerCase()]
|
||||
}
|
||||
|
||||
public static defaultKeys = [].concat(AllImageProviders.ImageAttributionSource.map(provider => provider.defaultKeyPrefixes))
|
||||
public static defaultKeys = [].concat(
|
||||
AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes)
|
||||
)
|
||||
|
||||
|
||||
private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<string, UIEventSource<ProvidedImage[]>>()
|
||||
private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<
|
||||
string,
|
||||
UIEventSource<ProvidedImage[]>
|
||||
>()
|
||||
|
||||
public static LoadImagesFor(tags: Store<any>, tagKey?: string[]): Store<ProvidedImage[]> {
|
||||
if (tags.data.id === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
const cacheKey = tags.data.id + tagKey
|
||||
|
@ -48,23 +55,21 @@ export default class AllImageProviders {
|
|||
return cached
|
||||
}
|
||||
|
||||
|
||||
const source = new UIEventSource([])
|
||||
this._cache.set(cacheKey, source)
|
||||
const allSources = []
|
||||
for (const imageProvider of AllImageProviders.ImageAttributionSource) {
|
||||
|
||||
let prefixes = imageProvider.defaultKeyPrefixes
|
||||
if (tagKey !== undefined) {
|
||||
prefixes = tagKey
|
||||
}
|
||||
|
||||
const singleSource = imageProvider.GetRelevantUrls(tags, {
|
||||
prefixes: prefixes
|
||||
prefixes: prefixes,
|
||||
})
|
||||
allSources.push(singleSource)
|
||||
singleSource.addCallbackAndRunD(_ => {
|
||||
const all: ProvidedImage[] = [].concat(...allSources.map(source => source.data))
|
||||
singleSource.addCallbackAndRunD((_) => {
|
||||
const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data))
|
||||
const uniq = []
|
||||
const seen = new Set<string>()
|
||||
for (const img of all) {
|
||||
|
@ -77,7 +82,6 @@ export default class AllImageProviders {
|
|||
source.setData(uniq)
|
||||
})
|
||||
}
|
||||
return source;
|
||||
return source
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import ImageProvider, {ProvidedImage} from "./ImageProvider";
|
||||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||
|
||||
export default class GenericImageProvider extends ImageProvider {
|
||||
public defaultKeyPrefixes: string[] = ["image"];
|
||||
public defaultKeyPrefixes: string[] = ["image"]
|
||||
|
||||
private readonly _valuePrefixBlacklist: string[];
|
||||
private readonly _valuePrefixBlacklist: string[]
|
||||
|
||||
public constructor(valuePrefixBlacklist: string[]) {
|
||||
super();
|
||||
this._valuePrefixBlacklist = valuePrefixBlacklist;
|
||||
super()
|
||||
this._valuePrefixBlacklist = valuePrefixBlacklist
|
||||
}
|
||||
|
||||
async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
|
||||
if (this._valuePrefixBlacklist.some(prefix => value.startsWith(prefix))) {
|
||||
if (this._valuePrefixBlacklist.some((prefix) => value.startsWith(prefix))) {
|
||||
return []
|
||||
}
|
||||
|
||||
|
@ -23,20 +22,20 @@ export default class GenericImageProvider extends ImageProvider {
|
|||
return []
|
||||
}
|
||||
|
||||
return [Promise.resolve({
|
||||
key: key,
|
||||
url: value,
|
||||
provider: this
|
||||
})]
|
||||
return [
|
||||
Promise.resolve({
|
||||
key: key,
|
||||
url: value,
|
||||
provider: this,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
SourceIcon(backlinkSource?: string) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
public DownloadAttribution(url: string) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +1,53 @@
|
|||
import {Store, UIEventSource} from "../UIEventSource";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import {LicenseInfo} from "./LicenseInfo";
|
||||
import {Utils} from "../../Utils";
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import { LicenseInfo } from "./LicenseInfo"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export interface ProvidedImage {
|
||||
url: string,
|
||||
key: string,
|
||||
url: string
|
||||
key: string
|
||||
provider: ImageProvider
|
||||
}
|
||||
|
||||
export default abstract class ImageProvider {
|
||||
|
||||
public abstract readonly defaultKeyPrefixes: string[]
|
||||
|
||||
public abstract SourceIcon(backlinkSource?: string): BaseUIElement;
|
||||
|
||||
public abstract SourceIcon(backlinkSource?: string): BaseUIElement
|
||||
|
||||
/**
|
||||
* Given a properies object, maps it onto _all_ the available pictures for this imageProvider
|
||||
*/
|
||||
public GetRelevantUrls(allTags: Store<any>, options?: {
|
||||
prefixes?: string[]
|
||||
}): UIEventSource<ProvidedImage[]> {
|
||||
public GetRelevantUrls(
|
||||
allTags: Store<any>,
|
||||
options?: {
|
||||
prefixes?: string[]
|
||||
}
|
||||
): UIEventSource<ProvidedImage[]> {
|
||||
const prefixes = options?.prefixes ?? this.defaultKeyPrefixes
|
||||
if (prefixes === undefined) {
|
||||
throw "No `defaultKeyPrefixes` defined by this image provider"
|
||||
}
|
||||
const relevantUrls = new UIEventSource<{ url: string; key: string; provider: ImageProvider }[]>([])
|
||||
const relevantUrls = new UIEventSource<
|
||||
{ url: string; key: string; provider: ImageProvider }[]
|
||||
>([])
|
||||
const seenValues = new Set<string>()
|
||||
allTags.addCallbackAndRunD(tags => {
|
||||
allTags.addCallbackAndRunD((tags) => {
|
||||
for (const key in tags) {
|
||||
if (!prefixes.some(prefix => key.startsWith(prefix))) {
|
||||
if (!prefixes.some((prefix) => key.startsWith(prefix))) {
|
||||
continue
|
||||
}
|
||||
const values = Utils.NoEmpty(tags[key]?.split(";")?.map(v => v.trim()) ?? [])
|
||||
const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? [])
|
||||
for (const value of values) {
|
||||
|
||||
if (seenValues.has(value)) {
|
||||
continue
|
||||
}
|
||||
seenValues.add(value)
|
||||
this.ExtractUrls(key, value).then(promises => {
|
||||
this.ExtractUrls(key, value).then((promises) => {
|
||||
for (const promise of promises ?? []) {
|
||||
if (promise === undefined) {
|
||||
continue
|
||||
}
|
||||
promise.then(providedImage => {
|
||||
promise.then((providedImage) => {
|
||||
if (providedImage === undefined) {
|
||||
return
|
||||
}
|
||||
|
@ -54,15 +57,12 @@ export default abstract class ImageProvider {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
return relevantUrls
|
||||
}
|
||||
|
||||
public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>;
|
||||
public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>
|
||||
|
||||
public abstract DownloadAttribution(url: string): Promise<LicenseInfo>;
|
||||
|
||||
}
|
||||
public abstract DownloadAttribution(url: string): Promise<LicenseInfo>
|
||||
}
|
||||
|
|
|
@ -1,107 +1,112 @@
|
|||
import $ from "jquery"
|
||||
import ImageProvider, {ProvidedImage} from "./ImageProvider";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import {Utils} from "../../Utils";
|
||||
import Constants from "../../Models/Constants";
|
||||
import {LicenseInfo} from "./LicenseInfo";
|
||||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import { Utils } from "../../Utils"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { LicenseInfo } from "./LicenseInfo"
|
||||
|
||||
export class Imgur extends ImageProvider {
|
||||
|
||||
public static readonly defaultValuePrefix = ["https://i.imgur.com"]
|
||||
public static readonly singleton = new Imgur();
|
||||
public readonly defaultKeyPrefixes: string[] = ["image"];
|
||||
public static readonly singleton = new Imgur()
|
||||
public readonly defaultKeyPrefixes: string[] = ["image"]
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
super()
|
||||
}
|
||||
|
||||
static uploadMultiple(
|
||||
title: string, description: string, blobs: FileList,
|
||||
handleSuccessfullUpload: ((imageURL: string) => Promise<void>),
|
||||
allDone: (() => void),
|
||||
onFail: ((reason: string) => void),
|
||||
offset: number = 0) {
|
||||
|
||||
title: string,
|
||||
description: string,
|
||||
blobs: FileList,
|
||||
handleSuccessfullUpload: (imageURL: string) => Promise<void>,
|
||||
allDone: () => void,
|
||||
onFail: (reason: string) => void,
|
||||
offset: number = 0
|
||||
) {
|
||||
if (blobs.length == offset) {
|
||||
allDone();
|
||||
return;
|
||||
allDone()
|
||||
return
|
||||
}
|
||||
const blob = blobs.item(offset);
|
||||
const self = this;
|
||||
this.uploadImage(title, description, blob,
|
||||
const blob = blobs.item(offset)
|
||||
const self = this
|
||||
this.uploadImage(
|
||||
title,
|
||||
description,
|
||||
blob,
|
||||
async (imageUrl) => {
|
||||
await handleSuccessfullUpload(imageUrl);
|
||||
await handleSuccessfullUpload(imageUrl)
|
||||
self.uploadMultiple(
|
||||
title, description, blobs,
|
||||
title,
|
||||
description,
|
||||
blobs,
|
||||
handleSuccessfullUpload,
|
||||
allDone,
|
||||
onFail,
|
||||
offset + 1);
|
||||
offset + 1
|
||||
)
|
||||
},
|
||||
onFail
|
||||
);
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
static uploadImage(title: string, description: string, blob: File,
|
||||
handleSuccessfullUpload: ((imageURL: string) => Promise<void>),
|
||||
onFail: (reason: string) => void) {
|
||||
static uploadImage(
|
||||
title: string,
|
||||
description: string,
|
||||
blob: File,
|
||||
handleSuccessfullUpload: (imageURL: string) => Promise<void>,
|
||||
onFail: (reason: string) => void
|
||||
) {
|
||||
const apiUrl = "https://api.imgur.com/3/image"
|
||||
const apiKey = Constants.ImgurApiKey
|
||||
|
||||
const apiUrl = 'https://api.imgur.com/3/image';
|
||||
const apiKey = Constants.ImgurApiKey;
|
||||
|
||||
const 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);
|
||||
const formData = new FormData()
|
||||
formData.append("image", blob)
|
||||
formData.append("title", title)
|
||||
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
|
||||
// Image URL available at response.data.link
|
||||
// @ts-ignore
|
||||
$.ajax(settings).done(async function (response) {
|
||||
response = JSON.parse(response);
|
||||
await handleSuccessfullUpload(response.data.link);
|
||||
}).fail((reason) => {
|
||||
console.log("Uploading to IMGUR failed", reason);
|
||||
// @ts-ignore
|
||||
onFail(reason);
|
||||
});
|
||||
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
|
||||
onFail(reason)
|
||||
})
|
||||
}
|
||||
|
||||
SourceIcon(): BaseUIElement {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
if (Imgur.defaultValuePrefix.some(prefix => value.startsWith(prefix))) {
|
||||
return [Promise.resolve({
|
||||
url: value,
|
||||
key: key,
|
||||
provider: this
|
||||
})]
|
||||
if (Imgur.defaultValuePrefix.some((prefix) => value.startsWith(prefix))) {
|
||||
return [
|
||||
Promise.resolve({
|
||||
url: value,
|
||||
key: key,
|
||||
provider: this,
|
||||
}),
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the attribution from attribution
|
||||
*
|
||||
*
|
||||
* const data = {"data":{"id":"I9t6B7B","title":"Station Knokke","description":"author:Pieter Vander Vennet\r\nlicense:CC-BY 4.0\r\nosmid:node\/9812712386","datetime":1655052078,"type":"image\/jpeg","animated":false,"width":2400,"height":1795,"size":910872,"views":2,"bandwidth":1821744,"vote":null,"favorite":false,"nsfw":false,"section":null,"account_url":null,"account_id":null,"is_ad":false,"in_most_viral":false,"has_sound":false,"tags":[],"ad_type":0,"ad_url":"","edited":"0","in_gallery":false,"link":"https:\/\/i.imgur.com\/I9t6B7B.jpg","ad_config":{"safeFlags":["not_in_gallery","share"],"highRiskFlags":[],"unsafeFlags":["sixth_mod_unsafe"],"wallUnsafeFlags":[],"showsAds":false,"showAdLevel":1}},"success":true,"status":200}
|
||||
* Utils.injectJsonDownloadForTests("https://api.imgur.com/3/image/E0RuAK3", data)
|
||||
* const licenseInfo = await Imgur.singleton.DownloadAttribution("https://i.imgur.com/E0RuAK3.jpg")
|
||||
|
@ -110,29 +115,27 @@ export class Imgur extends ImageProvider {
|
|||
* expected.artist = "Pieter Vander Vennet"
|
||||
* licenseInfo // => expected
|
||||
*/
|
||||
public async DownloadAttribution (url: string) : Promise<LicenseInfo> {
|
||||
const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0];
|
||||
public async DownloadAttribution(url: string): Promise<LicenseInfo> {
|
||||
const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0]
|
||||
|
||||
const apiUrl = 'https://api.imgur.com/3/image/' + hash;
|
||||
const response = await Utils.downloadJsonCached(apiUrl, 365*24*60*60,
|
||||
{Authorization: 'Client-ID ' + Constants.ImgurApiKey})
|
||||
const apiUrl = "https://api.imgur.com/3/image/" + hash
|
||||
const response = await Utils.downloadJsonCached(apiUrl, 365 * 24 * 60 * 60, {
|
||||
Authorization: "Client-ID " + Constants.ImgurApiKey,
|
||||
})
|
||||
|
||||
const descr: string = response.data.description ?? "";
|
||||
const data: any = {};
|
||||
const descr: string = response.data.description ?? ""
|
||||
const data: any = {}
|
||||
for (const tag of descr.split("\n")) {
|
||||
const kv = tag.split(":");
|
||||
const k = kv[0];
|
||||
data[k] = kv[1]?.replace(/\r/g, "");
|
||||
const kv = tag.split(":")
|
||||
const k = kv[0]
|
||||
data[k] = kv[1]?.replace(/\r/g, "")
|
||||
}
|
||||
|
||||
const licenseInfo = new LicenseInfo()
|
||||
|
||||
const licenseInfo = new LicenseInfo();
|
||||
|
||||
licenseInfo.licenseShortName = data.license;
|
||||
licenseInfo.artist = data.author;
|
||||
licenseInfo.licenseShortName = data.license
|
||||
licenseInfo.artist = data.author
|
||||
|
||||
return licenseInfo
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {Imgur} from "./Imgur";
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import { Imgur } from "./Imgur"
|
||||
|
||||
export default class ImgurUploader {
|
||||
|
||||
public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([]);
|
||||
public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([]);
|
||||
public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([]);
|
||||
public maxFileSizeInMegabytes = 10;
|
||||
private readonly _handleSuccessUrl: (string) => Promise<void>;
|
||||
public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([])
|
||||
public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([])
|
||||
public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([])
|
||||
public maxFileSizeInMegabytes = 10
|
||||
private readonly _handleSuccessUrl: (string) => Promise<void>
|
||||
|
||||
constructor(handleSuccessUrl: (string) => Promise<void>) {
|
||||
this._handleSuccessUrl = handleSuccessUrl;
|
||||
this._handleSuccessUrl = handleSuccessUrl
|
||||
}
|
||||
|
||||
public uploadMany(title: string, description: string, files: FileList): void {
|
||||
|
@ -19,25 +18,26 @@ export default class ImgurUploader {
|
|||
}
|
||||
this.queue.ping()
|
||||
|
||||
const self = this;
|
||||
const self = this
|
||||
this.queue.setData([...self.queue.data])
|
||||
Imgur.uploadMultiple(title,
|
||||
Imgur.uploadMultiple(
|
||||
title,
|
||||
description,
|
||||
files,
|
||||
async function (url) {
|
||||
console.log("File saved at", url);
|
||||
console.log("File saved at", url)
|
||||
self.success.data.push(url)
|
||||
self.success.ping();
|
||||
await self._handleSuccessUrl(url);
|
||||
self.success.ping()
|
||||
await self._handleSuccessUrl(url)
|
||||
},
|
||||
function () {
|
||||
console.log("All uploads completed");
|
||||
console.log("All uploads completed")
|
||||
},
|
||||
|
||||
function (failReason) {
|
||||
console.log("Upload failed due to ", failReason)
|
||||
self.failed.setData([...self.failed.data, failReason])
|
||||
}
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
export class LicenseInfo {
|
||||
title: string = ""
|
||||
artist: string = "";
|
||||
license: string = undefined;
|
||||
licenseShortName: string = "";
|
||||
usageTerms: string = "";
|
||||
attributionRequired: boolean = false;
|
||||
copyrighted: boolean = false;
|
||||
credit: string = "";
|
||||
description: string = "";
|
||||
artist: string = ""
|
||||
license: string = undefined
|
||||
licenseShortName: string = ""
|
||||
usageTerms: string = ""
|
||||
attributionRequired: boolean = false
|
||||
copyrighted: boolean = false
|
||||
credit: string = ""
|
||||
description: string = ""
|
||||
informationLocation: URL = undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
import ImageProvider, {ProvidedImage} from "./ImageProvider";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import Svg from "../../Svg";
|
||||
import {Utils} from "../../Utils";
|
||||
import {LicenseInfo} from "./LicenseInfo";
|
||||
import Constants from "../../Models/Constants";
|
||||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import Svg from "../../Svg"
|
||||
import { Utils } from "../../Utils"
|
||||
import { LicenseInfo } from "./LicenseInfo"
|
||||
import Constants from "../../Models/Constants"
|
||||
|
||||
export class Mapillary extends ImageProvider {
|
||||
|
||||
public static readonly singleton = new Mapillary();
|
||||
public static readonly singleton = new Mapillary()
|
||||
private static readonly valuePrefix = "https://a.mapillary.com"
|
||||
public static readonly valuePrefixes = [Mapillary.valuePrefix, "http://mapillary.com", "https://mapillary.com", "http://www.mapillary.com", "https://www.mapillary.com"]
|
||||
public static readonly valuePrefixes = [
|
||||
Mapillary.valuePrefix,
|
||||
"http://mapillary.com",
|
||||
"https://mapillary.com",
|
||||
"http://www.mapillary.com",
|
||||
"https://www.mapillary.com",
|
||||
]
|
||||
defaultKeyPrefixes = ["mapillary", "image"]
|
||||
|
||||
/**
|
||||
* Indicates that this is the same URL
|
||||
* Ignores 'stp' parameter
|
||||
*
|
||||
*
|
||||
* const a = "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8xm5SGLt20ETziNqzhhBd8b8S5GHLiIu8N6BbyqHFohFAQoaJJPG8i5yQiSwjYmEqXSfVeoCmpiyBJICEkQK98JOB21kkJoBS8VdhYa-Ty93lBnznQyesJBtKcb32foGut2Hgt10hEMWJbE3dDgA?stp=s1024x768&ccb=10-5&oh=00_AT-ZGTXHzihoaQYBILmEiAEKR64z_IWiTlcAYq_D7Ka0-Q&oe=6278C456&_nc_sid=122ab1"
|
||||
* const b = "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8xm5SGLt20ETziNqzhhBd8b8S5GHLiIu8N6BbyqHFohFAQoaJJPG8i5yQiSwjYmEqXSfVeoCmpiyBJICEkQK98JOB21kkJoBS8VdhYa-Ty93lBnznQyesJBtKcb32foGut2Hgt10hEMWJbE3dDgA?stp=s256x192&ccb=10-5&oh=00_AT9BZ1Rpc9zbY_uNu92A_4gj1joiy1b6VtgtLIu_7wh9Bg&oe=6278C456&_nc_sid=122ab1"
|
||||
* Mapillary.sameUrl(a, b) => true
|
||||
|
@ -28,9 +33,9 @@ export class Mapillary extends ImageProvider {
|
|||
const aUrl = new URL(a)
|
||||
const bUrl = new URL(b)
|
||||
if (aUrl.host !== bUrl.host || aUrl.pathname !== bUrl.pathname) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
let allSame = true;
|
||||
let allSame = true
|
||||
aUrl.searchParams.forEach((value, key) => {
|
||||
if (key === "stp") {
|
||||
// This is the key indicating the image size on mapillary; we ignore it
|
||||
|
@ -41,20 +46,18 @@ export class Mapillary extends ImageProvider {
|
|||
return
|
||||
}
|
||||
})
|
||||
return allSame;
|
||||
|
||||
return allSame
|
||||
} catch (e) {
|
||||
console.debug("Could not compare ", a, "and", b, "due to", e)
|
||||
}
|
||||
return false;
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the correct key for API v4.0
|
||||
*/
|
||||
private static ExtractKeyFromURL(value: string): number {
|
||||
let key: string;
|
||||
let key: string
|
||||
|
||||
const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/)
|
||||
if (newApiFormat !== null) {
|
||||
|
@ -62,7 +65,7 @@ export class Mapillary extends ImageProvider {
|
|||
} else if (value.startsWith(Mapillary.valuePrefix)) {
|
||||
key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1)
|
||||
} else if (value.match("[0-9]*")) {
|
||||
key = value;
|
||||
key = value
|
||||
}
|
||||
|
||||
const keyAsNumber = Number(key)
|
||||
|
@ -74,7 +77,7 @@ export class Mapillary extends ImageProvider {
|
|||
}
|
||||
|
||||
SourceIcon(backlinkSource?: string): BaseUIElement {
|
||||
return Svg.mapillary_svg();
|
||||
return Svg.mapillary_svg()
|
||||
}
|
||||
|
||||
async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
|
@ -83,26 +86,30 @@ export class Mapillary extends ImageProvider {
|
|||
|
||||
public async DownloadAttribution(url: string): Promise<LicenseInfo> {
|
||||
const license = new LicenseInfo()
|
||||
license.artist = "Contributor name unavailable";
|
||||
license.license = "CC BY-SA 4.0";
|
||||
license.artist = "Contributor name unavailable"
|
||||
license.license = "CC BY-SA 4.0"
|
||||
// license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
|
||||
license.attributionRequired = true;
|
||||
license.attributionRequired = true
|
||||
return license
|
||||
}
|
||||
|
||||
private async PrepareUrlAsync(key: string, value: string): Promise<ProvidedImage> {
|
||||
const mapillaryId = Mapillary.ExtractKeyFromURL(value)
|
||||
if (mapillaryId === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
const metadataUrl = 'https://graph.mapillary.com/' + mapillaryId + '?fields=thumb_1024_url&&access_token=' + Constants.mapillary_client_token_v4;
|
||||
const response = await Utils.downloadJsonCached(metadataUrl,60*60)
|
||||
const url = <string>response["thumb_1024_url"];
|
||||
const metadataUrl =
|
||||
"https://graph.mapillary.com/" +
|
||||
mapillaryId +
|
||||
"?fields=thumb_1024_url&&access_token=" +
|
||||
Constants.mapillary_client_token_v4
|
||||
const response = await Utils.downloadJsonCached(metadataUrl, 60 * 60)
|
||||
const url = <string>response["thumb_1024_url"]
|
||||
return {
|
||||
url: url,
|
||||
provider: this,
|
||||
key: key
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import ImageProvider, {ProvidedImage} from "./ImageProvider";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import Svg from "../../Svg";
|
||||
import {WikimediaImageProvider} from "./WikimediaImageProvider";
|
||||
import Wikidata from "../Web/Wikidata";
|
||||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import Svg from "../../Svg"
|
||||
import { WikimediaImageProvider } from "./WikimediaImageProvider"
|
||||
import Wikidata from "../Web/Wikidata"
|
||||
|
||||
export class WikidataImageProvider extends ImageProvider {
|
||||
|
||||
public static readonly singleton = new WikidataImageProvider()
|
||||
public readonly defaultKeyPrefixes = ["wikidata"]
|
||||
|
||||
|
@ -14,7 +13,7 @@ export class WikidataImageProvider extends ImageProvider {
|
|||
}
|
||||
|
||||
public SourceIcon(backlinkSource?: string): BaseUIElement {
|
||||
throw Svg.wikidata_svg();
|
||||
throw Svg.wikidata_svg()
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
|
@ -39,7 +38,10 @@ export class WikidataImageProvider extends ImageProvider {
|
|||
}
|
||||
|
||||
const commons = entity.commons
|
||||
if (commons !== undefined && (commons.startsWith("Category:") || commons.startsWith("File:"))) {
|
||||
if (
|
||||
commons !== undefined &&
|
||||
(commons.startsWith("Category:") || commons.startsWith("File:"))
|
||||
) {
|
||||
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, commons)
|
||||
allImages.push(...promises)
|
||||
}
|
||||
|
@ -47,7 +49,6 @@ export class WikidataImageProvider extends ImageProvider {
|
|||
}
|
||||
|
||||
public DownloadAttribution(url: string): Promise<any> {
|
||||
throw new Error("Method not implemented; shouldn't be needed!");
|
||||
throw new Error("Method not implemented; shouldn't be needed!")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +1,47 @@
|
|||
import ImageProvider, {ProvidedImage} from "./ImageProvider";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import Svg from "../../Svg";
|
||||
import Link from "../../UI/Base/Link";
|
||||
import {Utils} from "../../Utils";
|
||||
import {LicenseInfo} from "./LicenseInfo";
|
||||
import Wikimedia from "../Web/Wikimedia";
|
||||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import Svg from "../../Svg"
|
||||
import Link from "../../UI/Base/Link"
|
||||
import { Utils } from "../../Utils"
|
||||
import { LicenseInfo } from "./LicenseInfo"
|
||||
import Wikimedia from "../Web/Wikimedia"
|
||||
|
||||
/**
|
||||
* This module provides endpoints for wikimedia and others
|
||||
*/
|
||||
export class WikimediaImageProvider extends ImageProvider {
|
||||
|
||||
|
||||
public static readonly singleton = new WikimediaImageProvider();
|
||||
public static readonly commonsPrefixes = ["https://commons.wikimedia.org/wiki/", "https://upload.wikimedia.org", "File:"]
|
||||
public static readonly singleton = new WikimediaImageProvider()
|
||||
public static readonly commonsPrefixes = [
|
||||
"https://commons.wikimedia.org/wiki/",
|
||||
"https://upload.wikimedia.org",
|
||||
"File:",
|
||||
]
|
||||
private readonly commons_key = "wikimedia_commons"
|
||||
public readonly defaultKeyPrefixes = [this.commons_key, "image"]
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
super()
|
||||
}
|
||||
|
||||
private static ExtractFileName(url: string) {
|
||||
if (!url.startsWith("http")) {
|
||||
return url;
|
||||
return url
|
||||
}
|
||||
const path = new URL(url).pathname
|
||||
return path.substring(path.lastIndexOf("/") + 1);
|
||||
|
||||
return path.substring(path.lastIndexOf("/") + 1)
|
||||
}
|
||||
|
||||
private static PrepareUrl(value: string): string {
|
||||
|
||||
if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
|
||||
return value;
|
||||
return value
|
||||
}
|
||||
return (`https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(value)}?width=500&height=400`)
|
||||
return `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(
|
||||
value
|
||||
)}?width=500&height=400`
|
||||
}
|
||||
|
||||
private static startsWithCommonsPrefix(value: string): boolean {
|
||||
return WikimediaImageProvider.commonsPrefixes.some(prefix => value.startsWith(prefix))
|
||||
return WikimediaImageProvider.commonsPrefixes.some((prefix) => value.startsWith(prefix))
|
||||
}
|
||||
|
||||
private static removeCommonsPrefix(value: string): string {
|
||||
|
@ -49,7 +51,7 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
if (!value.startsWith("File:")) {
|
||||
value = "File:" + value
|
||||
}
|
||||
return value;
|
||||
return value
|
||||
}
|
||||
|
||||
for (const prefix of WikimediaImageProvider.commonsPrefixes) {
|
||||
|
@ -61,21 +63,20 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
return part
|
||||
}
|
||||
}
|
||||
return value;
|
||||
return value
|
||||
}
|
||||
|
||||
SourceIcon(backlink: string): BaseUIElement {
|
||||
const img = Svg.wikimedia_commons_white_svg()
|
||||
.SetStyle("width:2em;height: 2em");
|
||||
const img = Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em")
|
||||
if (backlink === undefined) {
|
||||
return img
|
||||
}
|
||||
|
||||
|
||||
return new Link(Svg.wikimedia_commons_white_img,
|
||||
`https://commons.wikimedia.org/wiki/${backlink}`, true)
|
||||
|
||||
|
||||
return new Link(
|
||||
Svg.wikimedia_commons_white_img,
|
||||
`https://commons.wikimedia.org/wiki/${backlink}`,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
public PrepUrl(value: string): ProvidedImage {
|
||||
|
@ -99,7 +100,9 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
if (value.startsWith("Category:")) {
|
||||
const urls = await Wikimedia.GetCategoryContents(value)
|
||||
return urls.filter(url => url.startsWith("File:")).map(image => Promise.resolve(this.UrlForImage(image)))
|
||||
return urls
|
||||
.filter((url) => url.startsWith("File:"))
|
||||
.map((image) => Promise.resolve(this.UrlForImage(image)))
|
||||
}
|
||||
if (value.startsWith("File:")) {
|
||||
return [Promise.resolve(this.UrlForImage(value))]
|
||||
|
@ -116,24 +119,30 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
filename = WikimediaImageProvider.ExtractFileName(filename)
|
||||
|
||||
if (filename === "") {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
const url = "https://en.wikipedia.org/w/" +
|
||||
const url =
|
||||
"https://en.wikipedia.org/w/" +
|
||||
"api.php?action=query&prop=imageinfo&iiprop=extmetadata&" +
|
||||
"titles=" + filename +
|
||||
"&format=json&origin=*";
|
||||
const data = await Utils.downloadJsonCached(url,365*24*60*60)
|
||||
const licenseInfo = new LicenseInfo();
|
||||
"titles=" +
|
||||
filename +
|
||||
"&format=json&origin=*"
|
||||
const data = await Utils.downloadJsonCached(url, 365 * 24 * 60 * 60)
|
||||
const licenseInfo = new LicenseInfo()
|
||||
const pageInfo = data.query.pages[-1]
|
||||
if (pageInfo === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
const license = (pageInfo.imageinfo ?? [])[0]?.extmetadata;
|
||||
|
||||
const license = (pageInfo.imageinfo ?? [])[0]?.extmetadata
|
||||
if (license === undefined) {
|
||||
console.warn("The file", filename, "has no usable metedata or license attached... Please fix the license info file yourself!")
|
||||
return undefined;
|
||||
console.warn(
|
||||
"The file",
|
||||
filename,
|
||||
"has no usable metedata or license attached... Please fix the license info file yourself!"
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
let title = pageInfo.title
|
||||
|
@ -145,26 +154,22 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
}
|
||||
|
||||
licenseInfo.title = title
|
||||
licenseInfo.artist = license.Artist?.value;
|
||||
licenseInfo.license = license.License?.value;
|
||||
licenseInfo.copyrighted = license.Copyrighted?.value;
|
||||
licenseInfo.attributionRequired = license.AttributionRequired?.value;
|
||||
licenseInfo.usageTerms = license.UsageTerms?.value;
|
||||
licenseInfo.licenseShortName = license.LicenseShortName?.value;
|
||||
licenseInfo.credit = license.Credit?.value;
|
||||
licenseInfo.description = license.ImageDescription?.value;
|
||||
licenseInfo.informationLocation = new URL("https://en.wikipedia.org/wiki/"+pageInfo.title)
|
||||
return licenseInfo;
|
||||
|
||||
licenseInfo.artist = license.Artist?.value
|
||||
licenseInfo.license = license.License?.value
|
||||
licenseInfo.copyrighted = license.Copyrighted?.value
|
||||
licenseInfo.attributionRequired = license.AttributionRequired?.value
|
||||
licenseInfo.usageTerms = license.UsageTerms?.value
|
||||
licenseInfo.licenseShortName = license.LicenseShortName?.value
|
||||
licenseInfo.credit = license.Credit?.value
|
||||
licenseInfo.description = license.ImageDescription?.value
|
||||
licenseInfo.informationLocation = new URL("https://en.wikipedia.org/wiki/" + pageInfo.title)
|
||||
return licenseInfo
|
||||
}
|
||||
|
||||
private UrlForImage(image: string): ProvidedImage {
|
||||
if (!image.startsWith("File:")) {
|
||||
image = "File:" + image
|
||||
}
|
||||
return {url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this}
|
||||
return { url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
import Constants from "../Models/Constants";
|
||||
import Constants from "../Models/Constants"
|
||||
|
||||
export default class Maproulette {
|
||||
/**
|
||||
* The API endpoint to use
|
||||
*/
|
||||
endpoint: string;
|
||||
/**
|
||||
* The API endpoint to use
|
||||
*/
|
||||
endpoint: string
|
||||
|
||||
/**
|
||||
* The API key to use for all requests
|
||||
*/
|
||||
private apiKey: string;
|
||||
/**
|
||||
* The API key to use for all requests
|
||||
*/
|
||||
private apiKey: string
|
||||
|
||||
/**
|
||||
* Creates a new Maproulette instance
|
||||
* @param endpoint The API endpoint to use
|
||||
*/
|
||||
constructor(endpoint: string = "https://maproulette.org/api/v2") {
|
||||
this.endpoint = endpoint;
|
||||
this.apiKey = Constants.MaprouletteApiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a task
|
||||
* @param taskId The task to close
|
||||
*/
|
||||
async closeTask(taskId: number): Promise<void> {
|
||||
const response = await fetch(`${this.endpoint}/task/${taskId}/1`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"apiKey": this.apiKey,
|
||||
},
|
||||
});
|
||||
if (response.status !== 304) {
|
||||
console.log(`Failed to close task: ${response.status}`);
|
||||
/**
|
||||
* Creates a new Maproulette instance
|
||||
* @param endpoint The API endpoint to use
|
||||
*/
|
||||
constructor(endpoint: string = "https://maproulette.org/api/v2") {
|
||||
this.endpoint = endpoint
|
||||
this.apiKey = Constants.MaprouletteApiKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a task
|
||||
* @param taskId The task to close
|
||||
*/
|
||||
async closeTask(taskId: number): Promise<void> {
|
||||
const response = await fetch(`${this.endpoint}/task/${taskId}/1`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
apiKey: this.apiKey,
|
||||
},
|
||||
})
|
||||
if (response.status !== 304) {
|
||||
console.log(`Failed to close task: ${response.status}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import SimpleMetaTaggers, {SimpleMetaTagger} from "./SimpleMetaTagger";
|
||||
import {ExtraFuncParams, ExtraFunctions} from "./ExtraFunctions";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import {ElementStorage} from "./ElementStorage";
|
||||
|
||||
import SimpleMetaTaggers, { SimpleMetaTagger } from "./SimpleMetaTagger"
|
||||
import { ExtraFuncParams, ExtraFunctions } from "./ExtraFunctions"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import { ElementStorage } from "./ElementStorage"
|
||||
|
||||
/**
|
||||
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
|
||||
|
@ -10,10 +9,8 @@ import {ElementStorage} from "./ElementStorage";
|
|||
* All metatags start with an underscore
|
||||
*/
|
||||
export default class MetaTagging {
|
||||
|
||||
|
||||
private static errorPrintCount = 0;
|
||||
private static readonly stopErrorOutputAt = 10;
|
||||
private static errorPrintCount = 0
|
||||
private static readonly stopErrorOutputAt = 10
|
||||
private static retaggingFuncCache = new Map<string, ((feature: any) => void)[]>()
|
||||
|
||||
/**
|
||||
|
@ -22,17 +19,19 @@ export default class MetaTagging {
|
|||
*
|
||||
* Returns true if at least one feature has changed properties
|
||||
*/
|
||||
public static addMetatags(features: { feature: any; freshness: Date }[],
|
||||
params: ExtraFuncParams,
|
||||
layer: LayerConfig,
|
||||
state?: { allElements?: ElementStorage },
|
||||
options?: {
|
||||
includeDates?: true | boolean,
|
||||
includeNonDates?: true | boolean,
|
||||
evaluateStrict?: false | boolean
|
||||
}): boolean {
|
||||
public static addMetatags(
|
||||
features: { feature: any; freshness: Date }[],
|
||||
params: ExtraFuncParams,
|
||||
layer: LayerConfig,
|
||||
state?: { allElements?: ElementStorage },
|
||||
options?: {
|
||||
includeDates?: true | boolean
|
||||
includeNonDates?: true | boolean
|
||||
evaluateStrict?: false | boolean
|
||||
}
|
||||
): boolean {
|
||||
if (features === undefined || features.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Recalculating metatags...")
|
||||
|
@ -52,51 +51,62 @@ export default class MetaTagging {
|
|||
// The calculated functions - per layer - which add the new keys
|
||||
const layerFuncs = this.createRetaggingFunc(layer, state)
|
||||
|
||||
let atLeastOneFeatureChanged = false;
|
||||
let atLeastOneFeatureChanged = false
|
||||
|
||||
for (let i = 0; i < features.length; i++) {
|
||||
const ff = features[i];
|
||||
const ff = features[i]
|
||||
const feature = ff.feature
|
||||
const freshness = ff.freshness
|
||||
let somethingChanged = false
|
||||
let definedTags = new Set(Object.getOwnPropertyNames(feature.properties))
|
||||
for (const metatag of metatagsToApply) {
|
||||
try {
|
||||
if (!metatag.keys.some(key => feature.properties[key] === undefined)) {
|
||||
if (!metatag.keys.some((key) => feature.properties[key] === undefined)) {
|
||||
// All keys are already defined, we probably already ran this one
|
||||
continue
|
||||
}
|
||||
|
||||
if (metatag.isLazy) {
|
||||
if (!metatag.keys.some(key => !definedTags.has(key))) {
|
||||
if (!metatag.keys.some((key) => !definedTags.has(key))) {
|
||||
// All keys are defined - lets skip!
|
||||
continue
|
||||
}
|
||||
somethingChanged = true;
|
||||
somethingChanged = true
|
||||
metatag.applyMetaTagsOnFeature(feature, freshness, layer, state)
|
||||
if(options?.evaluateStrict){
|
||||
if (options?.evaluateStrict) {
|
||||
for (const key of metatag.keys) {
|
||||
feature.properties[key]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const newValueAdded = metatag.applyMetaTagsOnFeature(feature, freshness, layer, state)
|
||||
const newValueAdded = metatag.applyMetaTagsOnFeature(
|
||||
feature,
|
||||
freshness,
|
||||
layer,
|
||||
state
|
||||
)
|
||||
/* Note that the expression:
|
||||
* `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)`
|
||||
* Is WRONG
|
||||
*
|
||||
* IF something changed is `true` due to an earlier run, it will short-circuit and _not_ evaluate the right hand of the OR,
|
||||
* thus not running an update!
|
||||
*/
|
||||
* `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)`
|
||||
* Is WRONG
|
||||
*
|
||||
* IF something changed is `true` due to an earlier run, it will short-circuit and _not_ evaluate the right hand of the OR,
|
||||
* thus not running an update!
|
||||
*/
|
||||
somethingChanged = newValueAdded || somethingChanged
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e, e.stack)
|
||||
console.error(
|
||||
"Could not calculate metatag for ",
|
||||
metatag.keys.join(","),
|
||||
":",
|
||||
e,
|
||||
e.stack
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (layerFuncs !== undefined) {
|
||||
let retaggingChanged = false;
|
||||
let retaggingChanged = false
|
||||
try {
|
||||
retaggingChanged = layerFuncs(params, feature)
|
||||
} catch (e) {
|
||||
|
@ -113,42 +123,62 @@ export default class MetaTagging {
|
|||
return atLeastOneFeatureChanged
|
||||
}
|
||||
|
||||
private static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] {
|
||||
const functions: ((feature: any) => any)[] = [];
|
||||
private static createFunctionsForFeature(
|
||||
layerId: string,
|
||||
calculatedTags: [string, string, boolean][]
|
||||
): ((feature: any) => void)[] {
|
||||
const functions: ((feature: any) => any)[] = []
|
||||
for (const entry of calculatedTags) {
|
||||
const key = entry[0]
|
||||
const code = entry[1];
|
||||
const code = entry[1]
|
||||
const isStrict = entry[2]
|
||||
if (code === undefined) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
const calculateAndAssign: ((feat: any) => any) = (feat) => {
|
||||
const calculateAndAssign: (feat: any) => any = (feat) => {
|
||||
try {
|
||||
let result = new Function("feat", "return " + code + ";")(feat);
|
||||
let result = new Function("feat", "return " + code + ";")(feat)
|
||||
if (result === "") {
|
||||
result === undefined
|
||||
}
|
||||
if (result !== undefined && typeof result !== "string") {
|
||||
// Make sure it is a string!
|
||||
result = JSON.stringify(result);
|
||||
result = JSON.stringify(result)
|
||||
}
|
||||
delete feat.properties[key]
|
||||
feat.properties[key] = result;
|
||||
feat.properties[key] = result
|
||||
return result
|
||||
} catch (e) {
|
||||
if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) {
|
||||
console.warn("Could not calculate a " + (isStrict ? "strict " : "") + " calculated tag for key " + key + " defined by " + code + " (in layer" + layerId + ") due to \n" + e + "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e, e.stack)
|
||||
MetaTagging.errorPrintCount++;
|
||||
console.warn(
|
||||
"Could not calculate a " +
|
||||
(isStrict ? "strict " : "") +
|
||||
" calculated tag for key " +
|
||||
key +
|
||||
" defined by " +
|
||||
code +
|
||||
" (in layer" +
|
||||
layerId +
|
||||
") due to \n" +
|
||||
e +
|
||||
"\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features",
|
||||
e,
|
||||
e.stack
|
||||
)
|
||||
MetaTagging.errorPrintCount++
|
||||
if (MetaTagging.errorPrintCount == MetaTagging.stopErrorOutputAt) {
|
||||
console.error("Got ", MetaTagging.stopErrorOutputAt, " errors calculating this metatagging - stopping output now")
|
||||
console.error(
|
||||
"Got ",
|
||||
MetaTagging.stopErrorOutputAt,
|
||||
" errors calculating this metatagging - stopping output now"
|
||||
)
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isStrict) {
|
||||
functions.push(calculateAndAssign)
|
||||
continue
|
||||
|
@ -162,15 +192,14 @@ export default class MetaTagging {
|
|||
enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this
|
||||
get: function () {
|
||||
return calculateAndAssign(feature)
|
||||
}
|
||||
},
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
functions.push(f)
|
||||
}
|
||||
return functions;
|
||||
return functions
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -179,39 +208,37 @@ export default class MetaTagging {
|
|||
* @param state
|
||||
* @private
|
||||
*/
|
||||
private static createRetaggingFunc(layer: LayerConfig, state):
|
||||
((params: ExtraFuncParams, feature: any) => boolean) {
|
||||
|
||||
const calculatedTags: [string, string, boolean][] = layer.calculatedTags;
|
||||
private static createRetaggingFunc(
|
||||
layer: LayerConfig,
|
||||
state
|
||||
): (params: ExtraFuncParams, feature: any) => boolean {
|
||||
const calculatedTags: [string, string, boolean][] = layer.calculatedTags
|
||||
if (calculatedTags === undefined || calculatedTags.length === 0) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
let functions: ((feature: any) => void)[] = MetaTagging.retaggingFuncCache.get(layer.id);
|
||||
let functions: ((feature: any) => void)[] = MetaTagging.retaggingFuncCache.get(layer.id)
|
||||
if (functions === undefined) {
|
||||
functions = MetaTagging.createFunctionsForFeature(layer.id, calculatedTags)
|
||||
MetaTagging.retaggingFuncCache.set(layer.id, functions)
|
||||
}
|
||||
|
||||
|
||||
return (params: ExtraFuncParams, feature) => {
|
||||
const tags = feature.properties
|
||||
if (tags === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
ExtraFunctions.FullPatchFeature(params, feature);
|
||||
ExtraFunctions.FullPatchFeature(params, feature)
|
||||
for (const f of functions) {
|
||||
f(feature);
|
||||
f(feature)
|
||||
}
|
||||
state?.allElements?.getEventSourceById(feature.properties.id)?.ping();
|
||||
state?.allElements?.getEventSourceById(feature.properties.id)?.ping()
|
||||
} catch (e) {
|
||||
console.error("Invalid syntax in calculated tags or some other error: ", e)
|
||||
}
|
||||
return true; // Something changed
|
||||
return true // Something changed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import {OsmNode, OsmRelation, OsmWay} from "../OsmObject";
|
||||
import { OsmNode, OsmRelation, OsmWay } from "../OsmObject"
|
||||
|
||||
/**
|
||||
* Represents a single change to an object
|
||||
*/
|
||||
export interface ChangeDescription {
|
||||
|
||||
/**
|
||||
* Metadata to be included in the changeset
|
||||
*/
|
||||
meta: {
|
||||
/*
|
||||
* The theme with which this changeset was made
|
||||
*/
|
||||
theme: string,
|
||||
* The theme with which this changeset was made
|
||||
*/
|
||||
theme: string
|
||||
/**
|
||||
* The type of the change
|
||||
*/
|
||||
|
@ -20,22 +19,22 @@ export interface ChangeDescription {
|
|||
/**
|
||||
* THe motivation for the change, e.g. 'deleted because does not exist anymore'
|
||||
*/
|
||||
specialMotivation?: string,
|
||||
specialMotivation?: string
|
||||
/**
|
||||
* Added by Changes.ts
|
||||
*/
|
||||
distanceToObject?: number
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifier of the object
|
||||
*/
|
||||
type: "node" | "way" | "relation",
|
||||
type: "node" | "way" | "relation"
|
||||
/**
|
||||
* Identifier of the object
|
||||
* Negative for new objects
|
||||
*/
|
||||
id: number,
|
||||
id: number
|
||||
|
||||
/**
|
||||
* All changes to tags
|
||||
|
@ -43,7 +42,7 @@ export interface ChangeDescription {
|
|||
*
|
||||
* Note that this list will only contain the _changes_ to the tags, not the full set of tags
|
||||
*/
|
||||
tags?: { k: string, v: string }[],
|
||||
tags?: { k: string; v: string }[]
|
||||
|
||||
/**
|
||||
* A change to the geometry:
|
||||
|
@ -51,17 +50,20 @@ export interface ChangeDescription {
|
|||
* 2) Change of way geometry
|
||||
* 3) Change of relation members (untested)
|
||||
*/
|
||||
changes?: {
|
||||
lat: number,
|
||||
lon: number
|
||||
} | {
|
||||
/* Coordinates are only used for rendering. They should be LON, LAT
|
||||
* */
|
||||
coordinates: [number, number][]
|
||||
nodes: number[],
|
||||
} | {
|
||||
members: { type: "node" | "way" | "relation", ref: number, role: string }[]
|
||||
}
|
||||
changes?:
|
||||
| {
|
||||
lat: number
|
||||
lon: number
|
||||
}
|
||||
| {
|
||||
/* Coordinates are only used for rendering. They should be LON, LAT
|
||||
* */
|
||||
coordinates: [number, number][]
|
||||
nodes: number[]
|
||||
}
|
||||
| {
|
||||
members: { type: "node" | "way" | "relation"; ref: number; role: string }[]
|
||||
}
|
||||
|
||||
/*
|
||||
Set to delete the object
|
||||
|
@ -70,7 +72,6 @@ export interface ChangeDescription {
|
|||
}
|
||||
|
||||
export class ChangeDescriptionTools {
|
||||
|
||||
/**
|
||||
* Rewrites all the ids in a changeDescription
|
||||
*
|
||||
|
@ -111,7 +112,7 @@ export class ChangeDescriptionTools {
|
|||
* const rewritten = ChangeDescriptionTools.rewriteIds(change, mapping)
|
||||
* rewritten.id // => 789
|
||||
* rewritten.changes["nodes"] // => [42,43,44, 68453]
|
||||
*
|
||||
*
|
||||
* // should rewrite ids in relationship members
|
||||
* const change = <ChangeDescription> {
|
||||
* type: "way",
|
||||
|
@ -130,44 +131,49 @@ export class ChangeDescriptionTools {
|
|||
* rewritten.changes["members"] // => [{type: "way", ref: 42, role: "outer"},{type: "way", ref: 48, role: "outer"}]
|
||||
*
|
||||
*/
|
||||
public static rewriteIds(change: ChangeDescription, mappings: Map<string, string>): ChangeDescription {
|
||||
public static rewriteIds(
|
||||
change: ChangeDescription,
|
||||
mappings: Map<string, string>
|
||||
): ChangeDescription {
|
||||
const key = change.type + "/" + change.id
|
||||
|
||||
const wayHasChangedNode = ((change.changes ?? {})["nodes"] ?? []).some(id => mappings.has("node/" + id));
|
||||
const relationHasChangedMembers = ((change.changes ?? {})["members"] ?? [])
|
||||
.some((obj:{type: string, ref: number}) => mappings.has(obj.type+"/" + obj.ref));
|
||||
const wayHasChangedNode = ((change.changes ?? {})["nodes"] ?? []).some((id) =>
|
||||
mappings.has("node/" + id)
|
||||
)
|
||||
const relationHasChangedMembers = ((change.changes ?? {})["members"] ?? []).some(
|
||||
(obj: { type: string; ref: number }) => mappings.has(obj.type + "/" + obj.ref)
|
||||
)
|
||||
|
||||
const hasSomeChange = mappings.has(key)
|
||||
|| wayHasChangedNode || relationHasChangedMembers
|
||||
if(hasSomeChange){
|
||||
change = {...change}
|
||||
const hasSomeChange = mappings.has(key) || wayHasChangedNode || relationHasChangedMembers
|
||||
if (hasSomeChange) {
|
||||
change = { ...change }
|
||||
}
|
||||
|
||||
|
||||
if (mappings.has(key)) {
|
||||
const [_, newId] = mappings.get(key).split("/")
|
||||
change.id = Number.parseInt(newId)
|
||||
}
|
||||
if(wayHasChangedNode){
|
||||
change.changes = {...change.changes}
|
||||
change.changes["nodes"] = change.changes["nodes"].map(id => {
|
||||
const key = "node/"+id
|
||||
if(!mappings.has(key)){
|
||||
if (wayHasChangedNode) {
|
||||
change.changes = { ...change.changes }
|
||||
change.changes["nodes"] = change.changes["nodes"].map((id) => {
|
||||
const key = "node/" + id
|
||||
if (!mappings.has(key)) {
|
||||
return id
|
||||
}
|
||||
const [_, newId] = mappings.get(key).split("/")
|
||||
return Number.parseInt(newId)
|
||||
})
|
||||
}
|
||||
if(relationHasChangedMembers){
|
||||
change.changes = {...change.changes}
|
||||
if (relationHasChangedMembers) {
|
||||
change.changes = { ...change.changes }
|
||||
change.changes["members"] = change.changes["members"].map(
|
||||
(obj:{type: string, ref: number}) => {
|
||||
const key = obj.type+"/"+obj.ref;
|
||||
if(!mappings.has(key)){
|
||||
(obj: { type: string; ref: number }) => {
|
||||
const key = obj.type + "/" + obj.ref
|
||||
if (!mappings.has(key)) {
|
||||
return obj
|
||||
}
|
||||
const [_, newId] = mappings.get(key).split("/")
|
||||
return {...obj, ref: Number.parseInt(newId)}
|
||||
return { ...obj, ref: Number.parseInt(newId) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -193,4 +199,4 @@ export class ChangeDescriptionTools {
|
|||
return r.asGeoJson().geometry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +1,44 @@
|
|||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import OsmChangeAction from "./OsmChangeAction"
|
||||
import { Changes } from "../Changes"
|
||||
|
||||
export default class ChangeLocationAction extends OsmChangeAction {
|
||||
private readonly _id: number;
|
||||
private readonly _newLonLat: [number, number];
|
||||
private readonly _meta: { theme: string; reason: string };
|
||||
private readonly _id: number
|
||||
private readonly _newLonLat: [number, number]
|
||||
private readonly _meta: { theme: string; reason: string }
|
||||
|
||||
constructor(id: string, newLonLat: [number, number], meta: {
|
||||
theme: string,
|
||||
reason: string
|
||||
}) {
|
||||
super(id, true);
|
||||
constructor(
|
||||
id: string,
|
||||
newLonLat: [number, number],
|
||||
meta: {
|
||||
theme: string
|
||||
reason: string
|
||||
}
|
||||
) {
|
||||
super(id, true)
|
||||
if (!id.startsWith("node/")) {
|
||||
throw "Invalid ID: only 'node/number' is accepted"
|
||||
}
|
||||
this._id = Number(id.substring("node/".length))
|
||||
this._newLonLat = newLonLat;
|
||||
this._meta = meta;
|
||||
this._newLonLat = newLonLat
|
||||
this._meta = meta
|
||||
}
|
||||
|
||||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
|
||||
const d: ChangeDescription = {
|
||||
changes: {
|
||||
lat: this._newLonLat[1],
|
||||
lon: this._newLonLat[0]
|
||||
lon: this._newLonLat[0],
|
||||
},
|
||||
type: "node",
|
||||
id: this._id, meta: {
|
||||
id: this._id,
|
||||
meta: {
|
||||
changeType: "move",
|
||||
theme: this._meta.theme,
|
||||
specialMotivation: this._meta.reason
|
||||
}
|
||||
|
||||
specialMotivation: this._meta.reason,
|
||||
},
|
||||
}
|
||||
|
||||
return [d]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,65 +1,77 @@
|
|||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import {TagsFilter} from "../../Tags/TagsFilter";
|
||||
import {OsmTags} from "../../../Models/OsmFeature";
|
||||
import OsmChangeAction from "./OsmChangeAction"
|
||||
import { Changes } from "../Changes"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||
import { OsmTags } from "../../../Models/OsmFeature"
|
||||
|
||||
export default class ChangeTagAction extends OsmChangeAction {
|
||||
private readonly _elementId: string;
|
||||
private readonly _tagsFilter: TagsFilter;
|
||||
private readonly _currentTags: Record<string, string> | OsmTags;
|
||||
private readonly _meta: { theme: string, changeType: string };
|
||||
private readonly _elementId: string
|
||||
private readonly _tagsFilter: TagsFilter
|
||||
private readonly _currentTags: Record<string, string> | OsmTags
|
||||
private readonly _meta: { theme: string; changeType: string }
|
||||
|
||||
constructor(elementId: string,
|
||||
tagsFilter: TagsFilter,
|
||||
currentTags: Record<string, string>, meta: {
|
||||
theme: string,
|
||||
changeType: "answer" | "soft-delete" | "add-image" | string
|
||||
}) {
|
||||
super(elementId, true);
|
||||
this._elementId = elementId;
|
||||
this._tagsFilter = tagsFilter;
|
||||
this._currentTags = currentTags;
|
||||
this._meta = meta;
|
||||
constructor(
|
||||
elementId: string,
|
||||
tagsFilter: TagsFilter,
|
||||
currentTags: Record<string, string>,
|
||||
meta: {
|
||||
theme: string
|
||||
changeType: "answer" | "soft-delete" | "add-image" | string
|
||||
}
|
||||
) {
|
||||
super(elementId, true)
|
||||
this._elementId = elementId
|
||||
this._tagsFilter = tagsFilter
|
||||
this._currentTags = currentTags
|
||||
this._meta = meta
|
||||
}
|
||||
|
||||
/**
|
||||
* Doublechecks that no stupid values are added
|
||||
*/
|
||||
private static checkChange(kv: { k: string, v: string }): { k: string, v: string } {
|
||||
const key = kv.k;
|
||||
const value = kv.v;
|
||||
private static checkChange(kv: { k: string; v: string }): { k: string; v: string } {
|
||||
const key = kv.k
|
||||
const value = kv.v
|
||||
if (key === undefined || key === null) {
|
||||
console.error("Invalid key:", key);
|
||||
return undefined;
|
||||
console.error("Invalid key:", key)
|
||||
return undefined
|
||||
}
|
||||
if (value === undefined || value === null) {
|
||||
console.error("Invalid value for ", key, ":", value);
|
||||
return undefined;
|
||||
console.error("Invalid value for ", key, ":", value)
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
console.error("Invalid value for ", key, "as it is not a string:", value)
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (key.startsWith(" ") || value.startsWith(" ") || value.endsWith(" ") || key.endsWith(" ")) {
|
||||
if (
|
||||
key.startsWith(" ") ||
|
||||
value.startsWith(" ") ||
|
||||
value.endsWith(" ") ||
|
||||
key.endsWith(" ")
|
||||
) {
|
||||
console.warn("Tag starts with or ends with a space - trimming anyway")
|
||||
}
|
||||
|
||||
return {k: key.trim(), v: value.trim()};
|
||||
return { k: key.trim(), v: value.trim() }
|
||||
}
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
const changedTags: { k: string, v: string }[] = this._tagsFilter.asChange(this._currentTags).map(ChangeTagAction.checkChange)
|
||||
const changedTags: { k: string; v: string }[] = this._tagsFilter
|
||||
.asChange(this._currentTags)
|
||||
.map(ChangeTagAction.checkChange)
|
||||
const typeId = this._elementId.split("/")
|
||||
const type = typeId[0]
|
||||
const id = Number(typeId [1])
|
||||
return [{
|
||||
type: <"node" | "way" | "relation">type,
|
||||
id: id,
|
||||
tags: changedTags,
|
||||
meta: this._meta
|
||||
}]
|
||||
const id = Number(typeId[1])
|
||||
return [
|
||||
{
|
||||
type: <"node" | "way" | "relation">type,
|
||||
id: id,
|
||||
tags: changedTags,
|
||||
meta: this._meta,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +1,69 @@
|
|||
import {OsmCreateAction} from "./OsmChangeAction";
|
||||
import {Tag} from "../../Tags/Tag";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import FeaturePipelineState from "../../State/FeaturePipelineState";
|
||||
import FeatureSource from "../../FeatureSource/FeatureSource";
|
||||
import CreateNewWayAction from "./CreateNewWayAction";
|
||||
import CreateWayWithPointReuseAction, {MergePointConfig} from "./CreateWayWithPointReuseAction";
|
||||
import {And} from "../../Tags/And";
|
||||
import {TagUtils} from "../../Tags/TagUtils";
|
||||
|
||||
import { OsmCreateAction } from "./OsmChangeAction"
|
||||
import { Tag } from "../../Tags/Tag"
|
||||
import { Changes } from "../Changes"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import FeaturePipelineState from "../../State/FeaturePipelineState"
|
||||
import FeatureSource from "../../FeatureSource/FeatureSource"
|
||||
import CreateNewWayAction from "./CreateNewWayAction"
|
||||
import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWithPointReuseAction"
|
||||
import { And } from "../../Tags/And"
|
||||
import { TagUtils } from "../../Tags/TagUtils"
|
||||
|
||||
/**
|
||||
* More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
|
||||
*/
|
||||
export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAction {
|
||||
public newElementId: string = undefined;
|
||||
public newElementIdNumber: number = undefined;
|
||||
private readonly _tags: Tag[];
|
||||
public newElementId: string = undefined
|
||||
public newElementIdNumber: number = undefined
|
||||
private readonly _tags: Tag[]
|
||||
private readonly createOuterWay: CreateWayWithPointReuseAction
|
||||
private readonly createInnerWays: CreateNewWayAction[]
|
||||
private readonly geojsonPreview: any;
|
||||
private readonly theme: string;
|
||||
private readonly changeType: "import" | "create" | string;
|
||||
private readonly geojsonPreview: any
|
||||
private readonly theme: string
|
||||
private readonly changeType: "import" | "create" | string
|
||||
|
||||
constructor(tags: Tag[],
|
||||
outerRingCoordinates: [number, number][],
|
||||
innerRingsCoordinates: [number, number][][],
|
||||
state: FeaturePipelineState,
|
||||
config: MergePointConfig[],
|
||||
changeType: "import" | "create" | string
|
||||
constructor(
|
||||
tags: Tag[],
|
||||
outerRingCoordinates: [number, number][],
|
||||
innerRingsCoordinates: [number, number][][],
|
||||
state: FeaturePipelineState,
|
||||
config: MergePointConfig[],
|
||||
changeType: "import" | "create" | string
|
||||
) {
|
||||
super(null, true);
|
||||
this._tags = [...tags, new Tag("type", "multipolygon")];
|
||||
this.changeType = changeType;
|
||||
super(null, true)
|
||||
this._tags = [...tags, new Tag("type", "multipolygon")]
|
||||
this.changeType = changeType
|
||||
this.theme = state?.layoutToUse?.id ?? ""
|
||||
this.createOuterWay = new CreateWayWithPointReuseAction([], outerRingCoordinates, state, config)
|
||||
this.createInnerWays = innerRingsCoordinates.map(ringCoordinates =>
|
||||
new CreateNewWayAction([],
|
||||
ringCoordinates.map(([lon, lat]) => ({lat, lon})),
|
||||
{theme: state?.layoutToUse?.id}))
|
||||
this.createOuterWay = new CreateWayWithPointReuseAction(
|
||||
[],
|
||||
outerRingCoordinates,
|
||||
state,
|
||||
config
|
||||
)
|
||||
this.createInnerWays = innerRingsCoordinates.map(
|
||||
(ringCoordinates) =>
|
||||
new CreateNewWayAction(
|
||||
[],
|
||||
ringCoordinates.map(([lon, lat]) => ({ lat, lon })),
|
||||
{ theme: state?.layoutToUse?.id }
|
||||
)
|
||||
)
|
||||
|
||||
this.geojsonPreview = {
|
||||
type: "Feature",
|
||||
properties: TagUtils.changeAsProperties(new And(this._tags).asChange({})),
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [
|
||||
outerRingCoordinates,
|
||||
...innerRingsCoordinates
|
||||
]
|
||||
}
|
||||
coordinates: [outerRingCoordinates, ...innerRingsCoordinates],
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async getPreview(): Promise<FeatureSource> {
|
||||
const outerPreview = await this.createOuterWay.getPreview()
|
||||
outerPreview.features.data.push({
|
||||
freshness: new Date(),
|
||||
feature: this.geojsonPreview
|
||||
feature: this.geojsonPreview,
|
||||
})
|
||||
return outerPreview
|
||||
}
|
||||
|
@ -66,13 +71,12 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
|
|||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
console.log("Running CMPWPRA")
|
||||
const descriptions: ChangeDescription[] = []
|
||||
descriptions.push(...await this.createOuterWay.CreateChangeDescriptions(changes));
|
||||
descriptions.push(...(await this.createOuterWay.CreateChangeDescriptions(changes)))
|
||||
for (const innerWay of this.createInnerWays) {
|
||||
descriptions.push(...await innerWay.CreateChangeDescriptions(changes))
|
||||
descriptions.push(...(await innerWay.CreateChangeDescriptions(changes)))
|
||||
}
|
||||
|
||||
|
||||
this.newElementIdNumber = changes.getNewID();
|
||||
this.newElementIdNumber = changes.getNewID()
|
||||
this.newElementId = "relation/" + this.newElementIdNumber
|
||||
descriptions.push({
|
||||
type: "relation",
|
||||
|
@ -80,24 +84,25 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
|
|||
tags: new And(this._tags).asChange({}),
|
||||
meta: {
|
||||
theme: this.theme,
|
||||
changeType: this.changeType
|
||||
changeType: this.changeType,
|
||||
},
|
||||
changes: {
|
||||
members: [
|
||||
{
|
||||
type: "way",
|
||||
ref: this.createOuterWay.newElementIdNumber,
|
||||
role: "outer"
|
||||
role: "outer",
|
||||
},
|
||||
// @ts-ignore
|
||||
...this.createInnerWays.map(a => ({type: "way", ref: a.newElementIdNumber, role: "inner"}))
|
||||
]
|
||||
}
|
||||
...this.createInnerWays.map((a) => ({
|
||||
type: "way",
|
||||
ref: a.newElementIdNumber,
|
||||
role: "inner",
|
||||
})),
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
return descriptions
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import {Tag} from "../../Tags/Tag";
|
||||
import {OsmCreateAction} from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import {And} from "../../Tags/And";
|
||||
import {OsmWay} from "../OsmObject";
|
||||
import {GeoOperations} from "../../GeoOperations";
|
||||
import { Tag } from "../../Tags/Tag"
|
||||
import { OsmCreateAction } from "./OsmChangeAction"
|
||||
import { Changes } from "../Changes"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import { And } from "../../Tags/And"
|
||||
import { OsmWay } from "../OsmObject"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
|
||||
export default class CreateNewNodeAction extends OsmCreateAction {
|
||||
|
||||
/**
|
||||
* Maps previously created points onto their assigned ID, to reuse the point if uplaoded
|
||||
* "lat,lon" --> id
|
||||
|
@ -15,46 +14,47 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
|||
private static readonly previouslyCreatedPoints = new Map<string, number>()
|
||||
public newElementId: string = undefined
|
||||
public newElementIdNumber: number = undefined
|
||||
private readonly _basicTags: Tag[];
|
||||
private readonly _lat: number;
|
||||
private readonly _lon: number;
|
||||
private readonly _snapOnto: OsmWay;
|
||||
private readonly _reusePointDistance: number;
|
||||
private meta: { changeType: "create" | "import"; theme: string; specialMotivation?: string };
|
||||
private readonly _reusePreviouslyCreatedPoint: boolean;
|
||||
private readonly _basicTags: Tag[]
|
||||
private readonly _lat: number
|
||||
private readonly _lon: number
|
||||
private readonly _snapOnto: OsmWay
|
||||
private readonly _reusePointDistance: number
|
||||
private meta: { changeType: "create" | "import"; theme: string; specialMotivation?: string }
|
||||
private readonly _reusePreviouslyCreatedPoint: boolean
|
||||
|
||||
|
||||
constructor(basicTags: Tag[],
|
||||
lat: number, lon: number,
|
||||
options: {
|
||||
allowReuseOfPreviouslyCreatedPoints?: boolean,
|
||||
snapOnto?: OsmWay,
|
||||
reusePointWithinMeters?: number,
|
||||
theme: string,
|
||||
changeType: "create" | "import" | null,
|
||||
specialMotivation?: string
|
||||
}) {
|
||||
constructor(
|
||||
basicTags: Tag[],
|
||||
lat: number,
|
||||
lon: number,
|
||||
options: {
|
||||
allowReuseOfPreviouslyCreatedPoints?: boolean
|
||||
snapOnto?: OsmWay
|
||||
reusePointWithinMeters?: number
|
||||
theme: string
|
||||
changeType: "create" | "import" | null
|
||||
specialMotivation?: string
|
||||
}
|
||||
) {
|
||||
super(null, basicTags !== undefined && basicTags.length > 0)
|
||||
this._basicTags = basicTags;
|
||||
this._lat = lat;
|
||||
this._lon = lon;
|
||||
this._basicTags = basicTags
|
||||
this._lat = lat
|
||||
this._lon = lon
|
||||
if (lat === undefined || lon === undefined) {
|
||||
throw "Lat or lon are undefined!"
|
||||
}
|
||||
this._snapOnto = options?.snapOnto;
|
||||
this._snapOnto = options?.snapOnto
|
||||
this._reusePointDistance = options?.reusePointWithinMeters ?? 1
|
||||
this._reusePreviouslyCreatedPoint = options?.allowReuseOfPreviouslyCreatedPoints ?? (basicTags.length === 0)
|
||||
this._reusePreviouslyCreatedPoint =
|
||||
options?.allowReuseOfPreviouslyCreatedPoints ?? basicTags.length === 0
|
||||
this.meta = {
|
||||
theme: options.theme,
|
||||
changeType: options.changeType,
|
||||
specialMotivation: options.specialMotivation
|
||||
specialMotivation: options.specialMotivation,
|
||||
}
|
||||
}
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
|
||||
if (this._reusePreviouslyCreatedPoint) {
|
||||
|
||||
const key = this._lat + "," + this._lon
|
||||
const prev = CreateNewNodeAction.previouslyCreatedPoints
|
||||
if (prev.has(key)) {
|
||||
|
@ -64,17 +64,23 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
const id = changes.getNewID()
|
||||
const properties = {
|
||||
id: "node/" + id
|
||||
id: "node/" + id,
|
||||
}
|
||||
this.setElementId(id)
|
||||
for (const kv of this._basicTags) {
|
||||
if (typeof kv.value !== "string") {
|
||||
throw "Invalid value: don't use non-string value in a preset. The tag "+kv.key+"="+kv.value+" is not a string, the value is a "+typeof kv.value
|
||||
throw (
|
||||
"Invalid value: don't use non-string value in a preset. The tag " +
|
||||
kv.key +
|
||||
"=" +
|
||||
kv.value +
|
||||
" is not a string, the value is a " +
|
||||
typeof kv.value
|
||||
)
|
||||
}
|
||||
properties[kv.key] = kv.value;
|
||||
properties[kv.key] = kv.value
|
||||
}
|
||||
|
||||
const newPointChange: ChangeDescription = {
|
||||
|
@ -83,32 +89,31 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
|||
id: id,
|
||||
changes: {
|
||||
lat: this._lat,
|
||||
lon: this._lon
|
||||
lon: this._lon,
|
||||
},
|
||||
meta: this.meta
|
||||
meta: this.meta,
|
||||
}
|
||||
if (this._snapOnto === undefined) {
|
||||
return [newPointChange]
|
||||
}
|
||||
|
||||
|
||||
// Project the point onto the way
|
||||
console.log("Snapping a node onto an existing way...")
|
||||
const geojson = this._snapOnto.asGeoJson()
|
||||
const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat])
|
||||
const projectedCoor= <[number, number]>projected.geometry.coordinates
|
||||
const projectedCoor = <[number, number]>projected.geometry.coordinates
|
||||
const index = projected.properties.index
|
||||
// We check that it isn't close to an already existing point
|
||||
let reusedPointId = undefined;
|
||||
let outerring : [number,number][];
|
||||
|
||||
if(geojson.geometry.type === "LineString"){
|
||||
outerring = <[number, number][]> geojson.geometry.coordinates
|
||||
}else if(geojson.geometry.type === "Polygon"){
|
||||
outerring =<[number, number][]> geojson.geometry.coordinates[0]
|
||||
let reusedPointId = undefined
|
||||
let outerring: [number, number][]
|
||||
|
||||
if (geojson.geometry.type === "LineString") {
|
||||
outerring = <[number, number][]>geojson.geometry.coordinates
|
||||
} else if (geojson.geometry.type === "Polygon") {
|
||||
outerring = <[number, number][]>geojson.geometry.coordinates[0]
|
||||
}
|
||||
|
||||
const prev= outerring[index]
|
||||
|
||||
const prev = outerring[index]
|
||||
if (GeoOperations.distanceBetween(prev, projectedCoor) < this._reusePointDistance) {
|
||||
// We reuse this point instead!
|
||||
reusedPointId = this._snapOnto.nodes[index]
|
||||
|
@ -120,20 +125,24 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
|||
}
|
||||
if (reusedPointId !== undefined) {
|
||||
this.setElementId(reusedPointId)
|
||||
return [{
|
||||
tags: new And(this._basicTags).asChange(properties),
|
||||
type: "node",
|
||||
id: reusedPointId,
|
||||
meta: this.meta
|
||||
}]
|
||||
return [
|
||||
{
|
||||
tags: new And(this._basicTags).asChange(properties),
|
||||
type: "node",
|
||||
id: reusedPointId,
|
||||
meta: this.meta,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const locations = [...this._snapOnto.coordinates.map(([lat, lon]) =><[number,number]> [lon, lat])]
|
||||
const locations = [
|
||||
...this._snapOnto.coordinates.map(([lat, lon]) => <[number, number]>[lon, lat]),
|
||||
]
|
||||
const ids = [...this._snapOnto.nodes]
|
||||
|
||||
locations.splice(index + 1, 0, [this._lon, this._lat])
|
||||
ids.splice(index + 1, 0, id)
|
||||
|
||||
|
||||
// Allright, we have to insert a new point in the way
|
||||
return [
|
||||
newPointChange,
|
||||
|
@ -142,15 +151,15 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
|||
id: this._snapOnto.id,
|
||||
changes: {
|
||||
coordinates: locations,
|
||||
nodes: ids
|
||||
nodes: ids,
|
||||
},
|
||||
meta: this.meta
|
||||
}
|
||||
meta: this.meta,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private setElementId(id: number) {
|
||||
this.newElementIdNumber = id;
|
||||
this.newElementIdNumber = id
|
||||
this.newElementId = "node/" + id
|
||||
if (!this._reusePreviouslyCreatedPoint) {
|
||||
return
|
||||
|
@ -158,6 +167,4 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
|||
const key = this._lat + "," + this._lon
|
||||
CreateNewNodeAction.previouslyCreatedPoints.set(key, id)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import {OsmCreateAction} from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import {Tag} from "../../Tags/Tag";
|
||||
import CreateNewNodeAction from "./CreateNewNodeAction";
|
||||
import {And} from "../../Tags/And";
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import { OsmCreateAction } from "./OsmChangeAction"
|
||||
import { Changes } from "../Changes"
|
||||
import { Tag } from "../../Tags/Tag"
|
||||
import CreateNewNodeAction from "./CreateNewNodeAction"
|
||||
import { And } from "../../Tags/And"
|
||||
|
||||
export default class CreateNewWayAction extends OsmCreateAction {
|
||||
public newElementId: string = undefined
|
||||
public newElementIdNumber: number = undefined;
|
||||
private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[];
|
||||
private readonly tags: Tag[];
|
||||
public newElementIdNumber: number = undefined
|
||||
private readonly coordinates: { nodeId?: number; lat: number; lon: number }[]
|
||||
private readonly tags: Tag[]
|
||||
private readonly _options: {
|
||||
theme: string
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/***
|
||||
* Creates a new way to upload to OSM
|
||||
|
@ -21,33 +20,44 @@ export default class CreateNewWayAction extends OsmCreateAction {
|
|||
* @param coordinates: the coordinates. Might have a nodeId, in this case, this node will be used
|
||||
* @param options
|
||||
*/
|
||||
constructor(tags: Tag[], coordinates: ({ nodeId?: number, lat: number, lon: number })[],
|
||||
options: {
|
||||
theme: string
|
||||
}) {
|
||||
constructor(
|
||||
tags: Tag[],
|
||||
coordinates: { nodeId?: number; lat: number; lon: number }[],
|
||||
options: {
|
||||
theme: string
|
||||
}
|
||||
) {
|
||||
super(null, true)
|
||||
this.coordinates = [];
|
||||
this.coordinates = []
|
||||
|
||||
for (const coordinate of coordinates) {
|
||||
/* The 'PointReuseAction' is a bit buggy and might generate duplicate ids.
|
||||
We filter those here, as the CreateWayWithPointReuseAction delegates the actual creation to here.
|
||||
Filtering here also prevents similar bugs in other actions
|
||||
*/
|
||||
if(this.coordinates.length > 0 && coordinate.nodeId !== undefined && this.coordinates[this.coordinates.length - 1].nodeId === coordinate.nodeId){
|
||||
if (
|
||||
this.coordinates.length > 0 &&
|
||||
coordinate.nodeId !== undefined &&
|
||||
this.coordinates[this.coordinates.length - 1].nodeId === coordinate.nodeId
|
||||
) {
|
||||
// This is a duplicate id
|
||||
console.warn("Skipping a node in createWay to avoid a duplicate node:", coordinate,"\nThe previous coordinates are: ", this.coordinates)
|
||||
console.warn(
|
||||
"Skipping a node in createWay to avoid a duplicate node:",
|
||||
coordinate,
|
||||
"\nThe previous coordinates are: ",
|
||||
this.coordinates
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
this.coordinates.push(coordinate)
|
||||
}
|
||||
|
||||
this.tags = tags;
|
||||
this._options = options;
|
||||
|
||||
this.tags = tags
|
||||
this._options = options
|
||||
}
|
||||
|
||||
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
|
||||
const newElements: ChangeDescription[] = []
|
||||
|
||||
const pointIds: number[] = []
|
||||
|
@ -60,16 +70,15 @@ export default class CreateNewWayAction extends OsmCreateAction {
|
|||
const newPoint = new CreateNewNodeAction([], coordinate.lat, coordinate.lon, {
|
||||
allowReuseOfPreviouslyCreatedPoints: true,
|
||||
changeType: null,
|
||||
theme: this._options.theme
|
||||
theme: this._options.theme,
|
||||
})
|
||||
newElements.push(...await newPoint.CreateChangeDescriptions(changes))
|
||||
newElements.push(...(await newPoint.CreateChangeDescriptions(changes)))
|
||||
pointIds.push(newPoint.newElementIdNumber)
|
||||
}
|
||||
|
||||
// We have all created (or reused) all the points!
|
||||
// Time to create the actual way
|
||||
|
||||
|
||||
const id = changes.getNewID()
|
||||
this.newElementIdNumber = id
|
||||
const newWay = <ChangeDescription>{
|
||||
|
@ -77,18 +86,16 @@ export default class CreateNewWayAction extends OsmCreateAction {
|
|||
type: "way",
|
||||
meta: {
|
||||
theme: this._options.theme,
|
||||
changeType: "import"
|
||||
changeType: "import",
|
||||
},
|
||||
tags: new And(this.tags).asChange({}),
|
||||
changes: {
|
||||
nodes: pointIds,
|
||||
coordinates: this.coordinates.map(c => [c.lon, c.lat])
|
||||
}
|
||||
coordinates: this.coordinates.map((c) => [c.lon, c.lat]),
|
||||
},
|
||||
}
|
||||
newElements.push(newWay)
|
||||
this.newElementId = "way/" + id
|
||||
return newElements
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import {OsmCreateAction} from "./OsmChangeAction";
|
||||
import {Tag} from "../../Tags/Tag";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import FeaturePipelineState from "../../State/FeaturePipelineState";
|
||||
import {BBox} from "../../BBox";
|
||||
import {TagsFilter} from "../../Tags/TagsFilter";
|
||||
import {GeoOperations} from "../../GeoOperations";
|
||||
import FeatureSource from "../../FeatureSource/FeatureSource";
|
||||
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource";
|
||||
import CreateNewNodeAction from "./CreateNewNodeAction";
|
||||
import CreateNewWayAction from "./CreateNewWayAction";
|
||||
|
||||
import { OsmCreateAction } from "./OsmChangeAction"
|
||||
import { Tag } from "../../Tags/Tag"
|
||||
import { Changes } from "../Changes"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import FeaturePipelineState from "../../State/FeaturePipelineState"
|
||||
import { BBox } from "../../BBox"
|
||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
import FeatureSource from "../../FeatureSource/FeatureSource"
|
||||
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
|
||||
import CreateNewNodeAction from "./CreateNewNodeAction"
|
||||
import CreateNewWayAction from "./CreateNewWayAction"
|
||||
|
||||
export interface MergePointConfig {
|
||||
withinRangeOfM: number,
|
||||
ifMatches: TagsFilter,
|
||||
withinRangeOfM: number
|
||||
ifMatches: TagsFilter
|
||||
mode: "reuse_osm_point" | "move_osm_point"
|
||||
}
|
||||
|
||||
|
@ -33,12 +32,12 @@ interface CoordinateInfo {
|
|||
/**
|
||||
* The new coordinate
|
||||
*/
|
||||
lngLat: [number, number],
|
||||
lngLat: [number, number]
|
||||
/**
|
||||
* If set: indicates that this point is identical to an earlier point in the way and that that point should be used.
|
||||
* This is especially needed in closed ways, where the last CoordinateInfo will have '0' as identicalTo
|
||||
*/
|
||||
identicalTo?: number,
|
||||
identicalTo?: number
|
||||
/**
|
||||
* Information about the closebyNode which might be reused
|
||||
*/
|
||||
|
@ -46,8 +45,8 @@ interface CoordinateInfo {
|
|||
/**
|
||||
* Distance in meters between the target coordinate and this candidate coordinate
|
||||
*/
|
||||
d: number,
|
||||
node: any,
|
||||
d: number
|
||||
node: any
|
||||
config: MergePointConfig
|
||||
}[]
|
||||
}
|
||||
|
@ -56,54 +55,55 @@ interface CoordinateInfo {
|
|||
* More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
|
||||
*/
|
||||
export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
||||
public newElementId: string = undefined;
|
||||
public newElementId: string = undefined
|
||||
public newElementIdNumber: number = undefined
|
||||
private readonly _tags: Tag[];
|
||||
private readonly _tags: Tag[]
|
||||
/**
|
||||
* lngLat-coordinates
|
||||
* @private
|
||||
*/
|
||||
private _coordinateInfo: CoordinateInfo[];
|
||||
private _state: FeaturePipelineState;
|
||||
private _config: MergePointConfig[];
|
||||
private _coordinateInfo: CoordinateInfo[]
|
||||
private _state: FeaturePipelineState
|
||||
private _config: MergePointConfig[]
|
||||
|
||||
constructor(tags: Tag[],
|
||||
coordinates: [number, number][],
|
||||
state: FeaturePipelineState,
|
||||
config: MergePointConfig[]
|
||||
constructor(
|
||||
tags: Tag[],
|
||||
coordinates: [number, number][],
|
||||
state: FeaturePipelineState,
|
||||
config: MergePointConfig[]
|
||||
) {
|
||||
super(null, true);
|
||||
this._tags = tags;
|
||||
this._state = state;
|
||||
this._config = config;
|
||||
super(null, true)
|
||||
this._tags = tags
|
||||
this._state = state
|
||||
this._config = config
|
||||
|
||||
// The main logic of this class: the coordinateInfo contains all the changes
|
||||
this._coordinateInfo = this.CalculateClosebyNodes(coordinates);
|
||||
|
||||
this._coordinateInfo = this.CalculateClosebyNodes(coordinates)
|
||||
}
|
||||
|
||||
public async getPreview(): Promise<FeatureSource> {
|
||||
|
||||
const features = []
|
||||
let geometryMoved = false;
|
||||
let geometryMoved = false
|
||||
for (let i = 0; i < this._coordinateInfo.length; i++) {
|
||||
const coordinateInfo = this._coordinateInfo[i];
|
||||
const coordinateInfo = this._coordinateInfo[i]
|
||||
if (coordinateInfo.identicalTo !== undefined) {
|
||||
continue
|
||||
}
|
||||
if (coordinateInfo.closebyNodes === undefined || coordinateInfo.closebyNodes.length === 0) {
|
||||
|
||||
if (
|
||||
coordinateInfo.closebyNodes === undefined ||
|
||||
coordinateInfo.closebyNodes.length === 0
|
||||
) {
|
||||
const newPoint = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"newpoint": "yes",
|
||||
id: "new-geometry-with-reuse-" + i
|
||||
newpoint: "yes",
|
||||
id: "new-geometry-with-reuse-" + i,
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: coordinateInfo.lngLat
|
||||
}
|
||||
};
|
||||
coordinates: coordinateInfo.lngLat,
|
||||
},
|
||||
}
|
||||
features.push(newPoint)
|
||||
continue
|
||||
}
|
||||
|
@ -113,18 +113,20 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
const moveDescription = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"move": "yes",
|
||||
move: "yes",
|
||||
"osm-id": reusedPoint.node.properties.id,
|
||||
"id": "new-geometry-move-existing" + i,
|
||||
"distance": GeoOperations.distanceBetween(coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates)
|
||||
id: "new-geometry-move-existing" + i,
|
||||
distance: GeoOperations.distanceBetween(
|
||||
coordinateInfo.lngLat,
|
||||
reusedPoint.node.geometry.coordinates
|
||||
),
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [reusedPoint.node.geometry.coordinates, coordinateInfo.lngLat]
|
||||
}
|
||||
coordinates: [reusedPoint.node.geometry.coordinates, coordinateInfo.lngLat],
|
||||
},
|
||||
}
|
||||
features.push(moveDescription)
|
||||
|
||||
} else {
|
||||
// The geometry is moved, the point is reused
|
||||
geometryMoved = true
|
||||
|
@ -132,22 +134,24 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
const reuseDescription = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"move": "no",
|
||||
move: "no",
|
||||
"osm-id": reusedPoint.node.properties.id,
|
||||
"id": "new-geometry-reuse-existing" + i,
|
||||
"distance": GeoOperations.distanceBetween(coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates)
|
||||
id: "new-geometry-reuse-existing" + i,
|
||||
distance: GeoOperations.distanceBetween(
|
||||
coordinateInfo.lngLat,
|
||||
reusedPoint.node.geometry.coordinates
|
||||
),
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates]
|
||||
}
|
||||
coordinates: [coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates],
|
||||
},
|
||||
}
|
||||
features.push(reuseDescription)
|
||||
}
|
||||
}
|
||||
|
||||
if (geometryMoved) {
|
||||
|
||||
const coords: [number, number][] = []
|
||||
for (const info of this._coordinateInfo) {
|
||||
if (info.identicalTo !== undefined) {
|
||||
|
@ -166,21 +170,19 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
} else {
|
||||
coords.push(info.lngLat)
|
||||
}
|
||||
|
||||
}
|
||||
const newGeometry = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"resulting-geometry": "yes",
|
||||
"id": "new-geometry"
|
||||
id: "new-geometry",
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: coords
|
||||
}
|
||||
coordinates: coords,
|
||||
},
|
||||
}
|
||||
features.push(newGeometry)
|
||||
|
||||
}
|
||||
return StaticFeatureSource.fromGeojson(features)
|
||||
}
|
||||
|
@ -188,7 +190,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
const theme = this._state?.layoutToUse?.id
|
||||
const allChanges: ChangeDescription[] = []
|
||||
const nodeIdsToUse: { lat: number, lon: number, nodeId?: number }[] = []
|
||||
const nodeIdsToUse: { lat: number; lon: number; nodeId?: number }[] = []
|
||||
for (let i = 0; i < this._coordinateInfo.length; i++) {
|
||||
const info = this._coordinateInfo[i]
|
||||
const lat = info.lngLat[1]
|
||||
|
@ -202,17 +204,17 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
const newNodeAction = new CreateNewNodeAction([], lat, lon, {
|
||||
allowReuseOfPreviouslyCreatedPoints: true,
|
||||
changeType: null,
|
||||
theme
|
||||
theme,
|
||||
})
|
||||
|
||||
allChanges.push(...(await newNodeAction.CreateChangeDescriptions(changes)))
|
||||
|
||||
nodeIdsToUse.push({
|
||||
lat, lon,
|
||||
nodeId: newNodeAction.newElementIdNumber
|
||||
lat,
|
||||
lon,
|
||||
nodeId: newNodeAction.newElementIdNumber,
|
||||
})
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
const closestPoint = info.closebyNodes[0]
|
||||
|
@ -222,20 +224,20 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
type: "node",
|
||||
id,
|
||||
changes: {
|
||||
lat, lon
|
||||
lat,
|
||||
lon,
|
||||
},
|
||||
meta: {
|
||||
theme,
|
||||
changeType: null
|
||||
}
|
||||
changeType: null,
|
||||
},
|
||||
})
|
||||
}
|
||||
nodeIdsToUse.push({lat, lon, nodeId: id})
|
||||
nodeIdsToUse.push({ lat, lon, nodeId: id })
|
||||
}
|
||||
|
||||
|
||||
const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, {
|
||||
theme
|
||||
theme,
|
||||
})
|
||||
|
||||
allChanges.push(...(await newWay.CreateChangeDescriptions(changes)))
|
||||
|
@ -248,27 +250,26 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
* Calculates the main changes.
|
||||
*/
|
||||
private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] {
|
||||
|
||||
const bbox = new BBox(coordinates)
|
||||
const state = this._state
|
||||
const allNodes = [].concat(...state?.featurePipeline?.GetFeaturesWithin("type_node", bbox.pad(1.2))??[])
|
||||
const maxDistance = Math.max(...this._config.map(c => c.withinRangeOfM))
|
||||
const allNodes = [].concat(
|
||||
...(state?.featurePipeline?.GetFeaturesWithin("type_node", bbox.pad(1.2)) ?? [])
|
||||
)
|
||||
const maxDistance = Math.max(...this._config.map((c) => c.withinRangeOfM))
|
||||
|
||||
// Init coordianteinfo with undefined but the same length as coordinates
|
||||
const coordinateInfo: {
|
||||
lngLat: [number, number],
|
||||
identicalTo?: number,
|
||||
lngLat: [number, number]
|
||||
identicalTo?: number
|
||||
closebyNodes?: {
|
||||
d: number,
|
||||
node: any,
|
||||
d: number
|
||||
node: any
|
||||
config: MergePointConfig
|
||||
}[]
|
||||
}[] = coordinates.map(_ => undefined)
|
||||
|
||||
}[] = coordinates.map((_) => undefined)
|
||||
|
||||
// First loop: gather all information...
|
||||
for (let i = 0; i < coordinates.length; i++) {
|
||||
|
||||
if (coordinateInfo[i] !== undefined) {
|
||||
// Already seen, probably a duplicate coordinate
|
||||
continue
|
||||
|
@ -282,9 +283,9 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
if (GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) {
|
||||
coordinateInfo[j] = {
|
||||
lngLat: coor,
|
||||
identicalTo: i
|
||||
identicalTo: i,
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -292,8 +293,8 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
|
||||
// Lets search applicable points and determine the merge mode
|
||||
const closebyNodes: {
|
||||
d: number,
|
||||
node: any,
|
||||
d: number
|
||||
node: any
|
||||
config: MergePointConfig
|
||||
}[] = []
|
||||
for (const node of allNodes) {
|
||||
|
@ -310,7 +311,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
if (!config.ifMatches.matchesProperties(node.properties)) {
|
||||
continue
|
||||
}
|
||||
closebyNodes.push({node, d, config})
|
||||
closebyNodes.push({ node, d, config })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -322,18 +323,15 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
coordinateInfo[i] = {
|
||||
identicalTo: undefined,
|
||||
lngLat: coor,
|
||||
closebyNodes
|
||||
closebyNodes,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Second loop: figure out which point moves where without creating conflicts
|
||||
let conflictFree = true;
|
||||
let conflictFree = true
|
||||
do {
|
||||
conflictFree = true;
|
||||
conflictFree = true
|
||||
for (let i = 0; i < coordinateInfo.length; i++) {
|
||||
|
||||
const coorInfo = coordinateInfo[i]
|
||||
if (coorInfo.identicalTo !== undefined) {
|
||||
continue
|
||||
|
@ -366,8 +364,6 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
}
|
||||
} while (!conflictFree)
|
||||
|
||||
|
||||
return coordinateInfo
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,61 +1,61 @@
|
|||
import {OsmObject} from "../OsmObject";
|
||||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import ChangeTagAction from "./ChangeTagAction";
|
||||
import {TagsFilter} from "../../Tags/TagsFilter";
|
||||
import {And} from "../../Tags/And";
|
||||
import {Tag} from "../../Tags/Tag";
|
||||
import { OsmObject } from "../OsmObject"
|
||||
import OsmChangeAction from "./OsmChangeAction"
|
||||
import { Changes } from "../Changes"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import ChangeTagAction from "./ChangeTagAction"
|
||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||
import { And } from "../../Tags/And"
|
||||
import { Tag } from "../../Tags/Tag"
|
||||
|
||||
export default class DeleteAction extends OsmChangeAction {
|
||||
|
||||
private readonly _softDeletionTags: TagsFilter;
|
||||
private readonly _softDeletionTags: TagsFilter
|
||||
private readonly meta: {
|
||||
theme: string,
|
||||
specialMotivation: string,
|
||||
theme: string
|
||||
specialMotivation: string
|
||||
changeType: "deletion"
|
||||
};
|
||||
private readonly _id: string;
|
||||
private _hardDelete: boolean;
|
||||
}
|
||||
private readonly _id: string
|
||||
private _hardDelete: boolean
|
||||
|
||||
|
||||
constructor(id: string,
|
||||
softDeletionTags: TagsFilter,
|
||||
meta: {
|
||||
theme: string,
|
||||
specialMotivation: string
|
||||
},
|
||||
hardDelete: boolean) {
|
||||
constructor(
|
||||
id: string,
|
||||
softDeletionTags: TagsFilter,
|
||||
meta: {
|
||||
theme: string
|
||||
specialMotivation: string
|
||||
},
|
||||
hardDelete: boolean
|
||||
) {
|
||||
super(id, true)
|
||||
this._id = id;
|
||||
this._hardDelete = hardDelete;
|
||||
this.meta = {...meta, changeType: "deletion"};
|
||||
this._softDeletionTags = new And([softDeletionTags,
|
||||
new Tag("fixme", `A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})`)
|
||||
]);
|
||||
|
||||
this._id = id
|
||||
this._hardDelete = hardDelete
|
||||
this.meta = { ...meta, changeType: "deletion" }
|
||||
this._softDeletionTags = new And([
|
||||
softDeletionTags,
|
||||
new Tag(
|
||||
"fixme",
|
||||
`A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})`
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
|
||||
const osmObject = await OsmObject.DownloadObjectAsync(this._id)
|
||||
|
||||
if (this._hardDelete) {
|
||||
return [{
|
||||
meta: this.meta,
|
||||
doDelete: true,
|
||||
type: osmObject.type,
|
||||
id: osmObject.id,
|
||||
}]
|
||||
} else {
|
||||
return await new ChangeTagAction(
|
||||
this._id, this._softDeletionTags, osmObject.tags,
|
||||
return [
|
||||
{
|
||||
...this.meta,
|
||||
changeType: "soft-delete"
|
||||
}
|
||||
).CreateChangeDescriptions(changes)
|
||||
meta: this.meta,
|
||||
doDelete: true,
|
||||
type: osmObject.type,
|
||||
id: osmObject.id,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
return await new ChangeTagAction(this._id, this._softDeletionTags, osmObject.tags, {
|
||||
...this.meta,
|
||||
changeType: "soft-delete",
|
||||
}).CreateChangeDescriptions(changes)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,22 +2,21 @@
|
|||
* An action is a change to the OSM-database
|
||||
* It will generate some new/modified/deleted objects, which are all bundled by the 'changes'-object
|
||||
*/
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import { Changes } from "../Changes"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
|
||||
export default abstract class OsmChangeAction {
|
||||
|
||||
public readonly trackStatistics: boolean;
|
||||
public readonly trackStatistics: boolean
|
||||
/**
|
||||
* The ID of the object that is the center of this change.
|
||||
* Null if the action creates a new object (at initialization)
|
||||
* Undefined if such an id does not make sense
|
||||
*/
|
||||
public readonly mainObjectId: string;
|
||||
public readonly mainObjectId: string
|
||||
private isUsed = false
|
||||
|
||||
constructor(mainObjectId: string, trackStatistics: boolean = true) {
|
||||
this.trackStatistics = trackStatistics;
|
||||
this.trackStatistics = trackStatistics
|
||||
this.mainObjectId = mainObjectId
|
||||
}
|
||||
|
||||
|
@ -25,7 +24,7 @@ export default abstract class OsmChangeAction {
|
|||
if (this.isUsed) {
|
||||
throw "This ChangeAction is already used"
|
||||
}
|
||||
this.isUsed = true;
|
||||
this.isUsed = true
|
||||
return this.CreateChangeDescriptions(changes)
|
||||
}
|
||||
|
||||
|
@ -33,8 +32,6 @@ export default abstract class OsmChangeAction {
|
|||
}
|
||||
|
||||
export abstract class OsmCreateAction extends OsmChangeAction {
|
||||
|
||||
public newElementId: string
|
||||
public newElementIdNumber: number
|
||||
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import {OsmObject, OsmRelation, OsmWay} from "../OsmObject";
|
||||
import OsmChangeAction from "./OsmChangeAction"
|
||||
import { Changes } from "../Changes"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import { OsmObject, OsmRelation, OsmWay } from "../OsmObject"
|
||||
|
||||
export interface RelationSplitInput {
|
||||
relation: OsmRelation,
|
||||
originalWayId: number,
|
||||
allWayIdsInOrder: number[],
|
||||
originalNodes: number[],
|
||||
relation: OsmRelation
|
||||
originalWayId: number
|
||||
allWayIdsInOrder: number[]
|
||||
originalNodes: number[]
|
||||
allWaysNodesInOrder: number[][]
|
||||
}
|
||||
|
||||
abstract class AbstractRelationSplitHandler extends OsmChangeAction {
|
||||
protected readonly _input: RelationSplitInput;
|
||||
protected readonly _theme: string;
|
||||
protected readonly _input: RelationSplitInput
|
||||
protected readonly _theme: string
|
||||
|
||||
constructor(input: RelationSplitInput, theme: string) {
|
||||
super("relation/" + input.relation.id, false)
|
||||
this._input = input;
|
||||
this._theme = theme;
|
||||
this._input = input
|
||||
this._theme = theme
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,7 +44,7 @@ abstract class AbstractRelationSplitHandler extends OsmChangeAction {
|
|||
if (member.type === "relation") {
|
||||
return undefined
|
||||
}
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,6 @@ abstract class AbstractRelationSplitHandler extends OsmChangeAction {
|
|||
* When a way is split and this way is part of a relation, the relation should be updated too to have the new segment if relevant.
|
||||
*/
|
||||
export default class RelationSplitHandler extends AbstractRelationSplitHandler {
|
||||
|
||||
constructor(input: RelationSplitInput, theme: string) {
|
||||
super(input, theme)
|
||||
}
|
||||
|
@ -60,38 +59,43 @@ export default class RelationSplitHandler extends AbstractRelationSplitHandler {
|
|||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
if (this._input.relation.tags["type"] === "restriction") {
|
||||
// This is a turn restriction
|
||||
return new TurnRestrictionRSH(this._input, this._theme).CreateChangeDescriptions(changes)
|
||||
return new TurnRestrictionRSH(this._input, this._theme).CreateChangeDescriptions(
|
||||
changes
|
||||
)
|
||||
}
|
||||
return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(changes)
|
||||
return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(
|
||||
changes
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export class TurnRestrictionRSH extends AbstractRelationSplitHandler {
|
||||
|
||||
constructor(input: RelationSplitInput, theme: string) {
|
||||
super(input, theme);
|
||||
super(input, theme)
|
||||
}
|
||||
|
||||
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
|
||||
const relation = this._input.relation
|
||||
const members = relation.members
|
||||
|
||||
const selfMembers = members.filter(m => m.type === "way" && m.ref === this._input.originalWayId)
|
||||
const selfMembers = members.filter(
|
||||
(m) => m.type === "way" && m.ref === this._input.originalWayId
|
||||
)
|
||||
|
||||
if (selfMembers.length > 1) {
|
||||
console.warn("Detected a turn restriction where this way has multiple occurances. This is an error")
|
||||
console.warn(
|
||||
"Detected a turn restriction where this way has multiple occurances. This is an error"
|
||||
)
|
||||
}
|
||||
const selfMember = selfMembers[0]
|
||||
|
||||
if (selfMember.role === "via") {
|
||||
// A via way can be replaced in place
|
||||
return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(changes);
|
||||
return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(
|
||||
changes
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// We have to keep only the way with a common point with the rest of the relation
|
||||
// Let's figure out which member is neighbouring our way
|
||||
|
||||
|
@ -102,11 +106,12 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler {
|
|||
let commonPoint = commonStartPoint ?? commonEndPoint
|
||||
|
||||
// Let's select the way to keep
|
||||
const idToKeep: { id: number } = this._input.allWaysNodesInOrder.map((nodes, i) => ({
|
||||
nodes: nodes,
|
||||
id: this._input.allWayIdsInOrder[i]
|
||||
}))
|
||||
.filter(nodesId => {
|
||||
const idToKeep: { id: number } = this._input.allWaysNodesInOrder
|
||||
.map((nodes, i) => ({
|
||||
nodes: nodes,
|
||||
id: this._input.allWayIdsInOrder[i],
|
||||
}))
|
||||
.filter((nodesId) => {
|
||||
const nds = nodesId.nodes
|
||||
return nds[0] == commonPoint || nds[nds.length - 1] == commonPoint
|
||||
})[0]
|
||||
|
@ -123,36 +128,34 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler {
|
|||
}
|
||||
|
||||
const newMembers: {
|
||||
ref: number,
|
||||
type: "way" | "node" | "relation",
|
||||
ref: number
|
||||
type: "way" | "node" | "relation"
|
||||
role: string
|
||||
} [] = relation.members.map(m => {
|
||||
}[] = relation.members.map((m) => {
|
||||
if (m.type === "way" && m.ref === originalWayId) {
|
||||
return {
|
||||
ref: idToKeep.id,
|
||||
type: "way",
|
||||
role: m.role
|
||||
role: m.role,
|
||||
}
|
||||
}
|
||||
return m
|
||||
})
|
||||
|
||||
|
||||
return [
|
||||
{
|
||||
type: "relation",
|
||||
id: relation.id,
|
||||
changes: {
|
||||
members: newMembers
|
||||
members: newMembers,
|
||||
},
|
||||
meta: {
|
||||
theme: this._theme,
|
||||
changeType: "relation-fix:turn_restriction"
|
||||
}
|
||||
}
|
||||
];
|
||||
changeType: "relation-fix:turn_restriction",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -163,26 +166,24 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler {
|
|||
* Note that the feature might appear multiple times.
|
||||
*/
|
||||
export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler {
|
||||
|
||||
constructor(input: RelationSplitInput, theme: string) {
|
||||
super(input, theme);
|
||||
super(input, theme)
|
||||
}
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
|
||||
const wayId = this._input.originalWayId
|
||||
const relation = this._input.relation
|
||||
const members = relation.members
|
||||
const originalNodes = this._input.originalNodes;
|
||||
const originalNodes = this._input.originalNodes
|
||||
const firstNode = originalNodes[0]
|
||||
const lastNode = originalNodes[originalNodes.length - 1]
|
||||
const newMembers: { type: "node" | "way" | "relation", ref: number, role: string }[] = []
|
||||
const newMembers: { type: "node" | "way" | "relation"; ref: number; role: string }[] = []
|
||||
|
||||
for (let i = 0; i < members.length; i++) {
|
||||
const member = members[i];
|
||||
const member = members[i]
|
||||
if (member.type !== "way" || member.ref !== wayId) {
|
||||
newMembers.push(member)
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
const nodeIdBefore = await this.targetNodeAt(i - 1, false)
|
||||
|
@ -197,10 +198,10 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler {
|
|||
newMembers.push({
|
||||
ref: wId,
|
||||
type: "way",
|
||||
role: member.role
|
||||
role: member.role,
|
||||
})
|
||||
}
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
const firstNodeMatchesRev = nodeIdBefore === undefined || nodeIdBefore === lastNode
|
||||
|
@ -209,14 +210,14 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler {
|
|||
// We (probably) have a reversed situation, backward situation
|
||||
for (let i1 = this._input.allWayIdsInOrder.length - 1; i1 >= 0; i1--) {
|
||||
// Iterate BACKWARDS
|
||||
const wId = this._input.allWayIdsInOrder[i1];
|
||||
const wId = this._input.allWayIdsInOrder[i1]
|
||||
newMembers.push({
|
||||
ref: wId,
|
||||
type: "way",
|
||||
role: member.role
|
||||
role: member.role,
|
||||
})
|
||||
}
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
// Euhm, allright... Something weird is going on, but let's not care too much
|
||||
|
@ -225,21 +226,21 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler {
|
|||
newMembers.push({
|
||||
ref: wId,
|
||||
type: "way",
|
||||
role: member.role
|
||||
role: member.role,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [{
|
||||
id: relation.id,
|
||||
type: "relation",
|
||||
changes: {members: newMembers},
|
||||
meta: {
|
||||
changeType: "relation-fix",
|
||||
theme: this._theme
|
||||
}
|
||||
}];
|
||||
return [
|
||||
{
|
||||
id: relation.id,
|
||||
type: "relation",
|
||||
changes: { members: newMembers },
|
||||
meta: {
|
||||
changeType: "relation-fix",
|
||||
theme: this._theme,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,59 +1,59 @@
|
|||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import {Tag} from "../../Tags/Tag";
|
||||
import FeatureSource from "../../FeatureSource/FeatureSource";
|
||||
import {OsmNode, OsmObject, OsmWay} from "../OsmObject";
|
||||
import {GeoOperations} from "../../GeoOperations";
|
||||
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource";
|
||||
import CreateNewNodeAction from "./CreateNewNodeAction";
|
||||
import ChangeTagAction from "./ChangeTagAction";
|
||||
import {And} from "../../Tags/And";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {OsmConnection} from "../OsmConnection";
|
||||
import {Feature} from "@turf/turf";
|
||||
import FeaturePipeline from "../../FeatureSource/FeaturePipeline";
|
||||
import OsmChangeAction from "./OsmChangeAction"
|
||||
import { Changes } from "../Changes"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import { Tag } from "../../Tags/Tag"
|
||||
import FeatureSource from "../../FeatureSource/FeatureSource"
|
||||
import { OsmNode, OsmObject, OsmWay } from "../OsmObject"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
|
||||
import CreateNewNodeAction from "./CreateNewNodeAction"
|
||||
import ChangeTagAction from "./ChangeTagAction"
|
||||
import { And } from "../../Tags/And"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { OsmConnection } from "../OsmConnection"
|
||||
import { Feature } from "@turf/turf"
|
||||
import FeaturePipeline from "../../FeatureSource/FeaturePipeline"
|
||||
|
||||
export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||
/**
|
||||
* The target feature - mostly used for the metadata
|
||||
*/
|
||||
private readonly feature: any;
|
||||
private readonly feature: any
|
||||
private readonly state: {
|
||||
osmConnection: OsmConnection,
|
||||
osmConnection: OsmConnection
|
||||
featurePipeline: FeaturePipeline
|
||||
};
|
||||
private readonly wayToReplaceId: string;
|
||||
private readonly theme: string;
|
||||
}
|
||||
private readonly wayToReplaceId: string
|
||||
private readonly theme: string
|
||||
/**
|
||||
* The target coordinates that should end up in OpenStreetMap.
|
||||
* This is identical to either this.feature.geometry.coordinates or -in case of a polygon- feature.geometry.coordinates[0]
|
||||
* Format: [lon, lat]
|
||||
*/
|
||||
private readonly targetCoordinates: [number, number][];
|
||||
private readonly targetCoordinates: [number, number][]
|
||||
/**
|
||||
* If a target coordinate is close to another target coordinate, 'identicalTo' will point to the first index.
|
||||
*/
|
||||
private readonly identicalTo: number[]
|
||||
private readonly newTags: Tag[] | undefined;
|
||||
private readonly newTags: Tag[] | undefined
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
osmConnection: OsmConnection,
|
||||
osmConnection: OsmConnection
|
||||
featurePipeline: FeaturePipeline
|
||||
},
|
||||
feature: any,
|
||||
wayToReplaceId: string,
|
||||
options: {
|
||||
theme: string,
|
||||
theme: string
|
||||
newTags?: Tag[]
|
||||
}
|
||||
) {
|
||||
super(wayToReplaceId, false);
|
||||
this.state = state;
|
||||
this.feature = feature;
|
||||
this.wayToReplaceId = wayToReplaceId;
|
||||
this.theme = options.theme;
|
||||
super(wayToReplaceId, false)
|
||||
this.state = state
|
||||
this.feature = feature
|
||||
this.wayToReplaceId = wayToReplaceId
|
||||
this.theme = options.theme
|
||||
|
||||
const geom = this.feature.geometry
|
||||
let coordinates: [number, number][]
|
||||
|
@ -64,7 +64,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
}
|
||||
this.targetCoordinates = coordinates
|
||||
|
||||
this.identicalTo = coordinates.map(_ => undefined)
|
||||
this.identicalTo = coordinates.map((_) => undefined)
|
||||
|
||||
for (let i = 0; i < coordinates.length; i++) {
|
||||
if (this.identicalTo[i] !== undefined) {
|
||||
|
@ -82,7 +82,8 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public async getPreview(): Promise<FeatureSource> {
|
||||
const {closestIds, allNodesById, detachedNodes, reprojectedNodes} = await this.GetClosestIds();
|
||||
const { closestIds, allNodesById, detachedNodes, reprojectedNodes } =
|
||||
await this.GetClosestIds()
|
||||
const preview: Feature[] = closestIds.map((newId, i) => {
|
||||
if (this.identicalTo[i] !== undefined) {
|
||||
return undefined
|
||||
|
@ -92,75 +93,73 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
return {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"newpoint": "yes",
|
||||
"id": "replace-geometry-move-" + i,
|
||||
newpoint: "yes",
|
||||
id: "replace-geometry-move-" + i,
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: this.targetCoordinates[i]
|
||||
}
|
||||
};
|
||||
coordinates: this.targetCoordinates[i],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const origNode = allNodesById.get(newId);
|
||||
const origNode = allNodesById.get(newId)
|
||||
return {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"move": "yes",
|
||||
move: "yes",
|
||||
"osm-id": newId,
|
||||
"id": "replace-geometry-move-" + i,
|
||||
"original-node-tags": JSON.stringify(origNode.tags)
|
||||
id: "replace-geometry-move-" + i,
|
||||
"original-node-tags": JSON.stringify(origNode.tags),
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [[origNode.lon, origNode.lat], this.targetCoordinates[i]]
|
||||
}
|
||||
};
|
||||
coordinates: [[origNode.lon, origNode.lat], this.targetCoordinates[i]],
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
reprojectedNodes.forEach(({newLat, newLon, nodeId}) => {
|
||||
|
||||
const origNode = allNodesById.get(nodeId);
|
||||
const feature : Feature = {
|
||||
reprojectedNodes.forEach(({ newLat, newLon, nodeId }) => {
|
||||
const origNode = allNodesById.get(nodeId)
|
||||
const feature: Feature = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"move": "yes",
|
||||
"reprojection": "yes",
|
||||
move: "yes",
|
||||
reprojection: "yes",
|
||||
"osm-id": nodeId,
|
||||
"id": "replace-geometry-reproject-" + nodeId,
|
||||
"original-node-tags": JSON.stringify(origNode.tags)
|
||||
id: "replace-geometry-reproject-" + nodeId,
|
||||
"original-node-tags": JSON.stringify(origNode.tags),
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [[origNode.lon, origNode.lat], [newLon, newLat]]
|
||||
}
|
||||
};
|
||||
coordinates: [
|
||||
[origNode.lon, origNode.lat],
|
||||
[newLon, newLat],
|
||||
],
|
||||
},
|
||||
}
|
||||
preview.push(feature)
|
||||
})
|
||||
|
||||
|
||||
detachedNodes.forEach(({reason}, id) => {
|
||||
const origNode = allNodesById.get(id);
|
||||
const feature : Feature = {
|
||||
detachedNodes.forEach(({ reason }, id) => {
|
||||
const origNode = allNodesById.get(id)
|
||||
const feature: Feature = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"detach": "yes",
|
||||
"id": "replace-geometry-detach-" + id,
|
||||
detach: "yes",
|
||||
id: "replace-geometry-detach-" + id,
|
||||
"detach-reason": reason,
|
||||
"original-node-tags": JSON.stringify(origNode.tags)
|
||||
"original-node-tags": JSON.stringify(origNode.tags),
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [origNode.lon, origNode.lat]
|
||||
}
|
||||
};
|
||||
coordinates: [origNode.lon, origNode.lat],
|
||||
},
|
||||
}
|
||||
preview.push(feature)
|
||||
})
|
||||
|
||||
|
||||
return StaticFeatureSource.fromGeojson(Utils.NoNull(preview))
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -170,45 +169,52 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
*
|
||||
*/
|
||||
public async GetClosestIds(): Promise<{
|
||||
|
||||
// A list of the same length as targetCoordinates, containing which OSM-point to move. If undefined, a new point will be created
|
||||
closestIds: number[],
|
||||
allNodesById: Map<number, OsmNode>,
|
||||
osmWay: OsmWay,
|
||||
detachedNodes: Map<number, {
|
||||
reason: string,
|
||||
hasTags: boolean
|
||||
}>,
|
||||
reprojectedNodes: Map<number, {
|
||||
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
|
||||
projectAfterIndex: number,
|
||||
distance: number,
|
||||
newLat: number,
|
||||
newLon: number,
|
||||
nodeId: number
|
||||
}>
|
||||
closestIds: number[]
|
||||
allNodesById: Map<number, OsmNode>
|
||||
osmWay: OsmWay
|
||||
detachedNodes: Map<
|
||||
number,
|
||||
{
|
||||
reason: string
|
||||
hasTags: boolean
|
||||
}
|
||||
>
|
||||
reprojectedNodes: Map<
|
||||
number,
|
||||
{
|
||||
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
|
||||
projectAfterIndex: number
|
||||
distance: number
|
||||
newLat: number
|
||||
newLon: number
|
||||
nodeId: number
|
||||
}
|
||||
>
|
||||
}> {
|
||||
// TODO FIXME: if a new point has to be created, snap to already existing ways
|
||||
|
||||
const nodeDb = this.state.featurePipeline.fullNodeDatabase;
|
||||
const nodeDb = this.state.featurePipeline.fullNodeDatabase
|
||||
if (nodeDb === undefined) {
|
||||
throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
|
||||
}
|
||||
const self = this;
|
||||
let parsed: OsmObject[];
|
||||
const self = this
|
||||
let parsed: OsmObject[]
|
||||
{
|
||||
// Gather the needed OsmObjects
|
||||
const splitted = this.wayToReplaceId.split("/");
|
||||
const type = splitted[0];
|
||||
const idN = Number(splitted[1]);
|
||||
const splitted = this.wayToReplaceId.split("/")
|
||||
const type = splitted[0]
|
||||
const idN = Number(splitted[1])
|
||||
if (idN < 0 || type !== "way") {
|
||||
throw "Invalid ID to conflate: " + this.wayToReplaceId
|
||||
}
|
||||
const url = `${this.state.osmConnection?._oauth_config?.url ?? "https://openstreetmap.org"}/api/0.6/${this.wayToReplaceId}/full`;
|
||||
const url = `${
|
||||
this.state.osmConnection?._oauth_config?.url ?? "https://openstreetmap.org"
|
||||
}/api/0.6/${this.wayToReplaceId}/full`
|
||||
const rawData = await Utils.downloadJsonCached(url, 1000)
|
||||
parsed = OsmObject.ParseObjects(rawData.elements);
|
||||
parsed = OsmObject.ParseObjects(rawData.elements)
|
||||
}
|
||||
const allNodes = parsed.filter(o => o.type === "node")
|
||||
const allNodes = parsed.filter((o) => o.type === "node")
|
||||
const osmWay = <OsmWay>parsed[parsed.length - 1]
|
||||
if (osmWay.type !== "way") {
|
||||
throw "WEIRD: expected an OSM-way as last element here!"
|
||||
|
@ -228,38 +234,42 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
*
|
||||
* The Replace-geometry action should try its best to honour these. Some 'wiggling' is allowed (e.g. moving an entrance a bit), but these relations should not be broken.l
|
||||
*/
|
||||
const distances = new Map<number /* osmId*/,
|
||||
const distances = new Map<
|
||||
number /* osmId*/,
|
||||
/** target coordinate index --> distance (or undefined if a duplicate)*/
|
||||
number[]>();
|
||||
number[]
|
||||
>()
|
||||
|
||||
const nodeInfo = new Map<number /* osmId*/, {
|
||||
distances: number[],
|
||||
// Part of some other way then the one that should be replaced
|
||||
partOfWay: boolean,
|
||||
hasTags: boolean
|
||||
}>()
|
||||
const nodeInfo = new Map<
|
||||
number /* osmId*/,
|
||||
{
|
||||
distances: number[]
|
||||
// Part of some other way then the one that should be replaced
|
||||
partOfWay: boolean
|
||||
hasTags: boolean
|
||||
}
|
||||
>()
|
||||
|
||||
for (const node of allNodes) {
|
||||
|
||||
const parentWays = nodeDb.GetParentWays(node.id)
|
||||
if (parentWays === undefined) {
|
||||
throw "PANIC: the way to replace has a node which has no parents at all. Is it deleted in the meantime?"
|
||||
}
|
||||
const parentWayIds = parentWays.data.map(w => w.type + "/" + w.id)
|
||||
const parentWayIds = parentWays.data.map((w) => w.type + "/" + w.id)
|
||||
const idIndex = parentWayIds.indexOf(this.wayToReplaceId)
|
||||
if (idIndex < 0) {
|
||||
throw "PANIC: the way to replace has a node, which is _not_ part of this was according to the node..."
|
||||
}
|
||||
parentWayIds.splice(idIndex, 1)
|
||||
const partOfSomeWay = parentWayIds.length > 0
|
||||
const hasTags = Object.keys(node.tags).length > 1;
|
||||
const hasTags = Object.keys(node.tags).length > 1
|
||||
|
||||
const nodeDistances = this.targetCoordinates.map(_ => undefined)
|
||||
const nodeDistances = this.targetCoordinates.map((_) => undefined)
|
||||
for (let i = 0; i < this.targetCoordinates.length; i++) {
|
||||
if (this.identicalTo[i] !== undefined) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
const targetCoordinate = this.targetCoordinates[i];
|
||||
const targetCoordinate = this.targetCoordinates[i]
|
||||
const cp = node.centerpoint()
|
||||
const d = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]])
|
||||
if (d > 25) {
|
||||
|
@ -268,37 +278,39 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
}
|
||||
if (d < 3 || !(hasTags || partOfSomeWay)) {
|
||||
// If there is some relation: cap the move distance to 3m
|
||||
nodeDistances[i] = d;
|
||||
nodeDistances[i] = d
|
||||
}
|
||||
|
||||
}
|
||||
distances.set(node.id, nodeDistances)
|
||||
nodeInfo.set(node.id, {
|
||||
distances: nodeDistances,
|
||||
partOfWay: partOfSomeWay,
|
||||
hasTags
|
||||
hasTags,
|
||||
})
|
||||
}
|
||||
|
||||
const closestIds = this.targetCoordinates.map(_ => undefined)
|
||||
const unusedIds = new Map<number, {
|
||||
reason: string,
|
||||
hasTags: boolean
|
||||
}>();
|
||||
const closestIds = this.targetCoordinates.map((_) => undefined)
|
||||
const unusedIds = new Map<
|
||||
number,
|
||||
{
|
||||
reason: string
|
||||
hasTags: boolean
|
||||
}
|
||||
>()
|
||||
{
|
||||
// Search best merge candidate
|
||||
/**
|
||||
* Then, we search the node that has to move the least distance and add this as mapping.
|
||||
* We do this until no points are left
|
||||
*/
|
||||
let candidate: number;
|
||||
let moveDistance: number;
|
||||
let candidate: number
|
||||
let moveDistance: number
|
||||
/**
|
||||
* The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates
|
||||
*/
|
||||
do {
|
||||
candidate = undefined;
|
||||
moveDistance = Infinity;
|
||||
candidate = undefined
|
||||
moveDistance = Infinity
|
||||
distances.forEach((distances, nodeId) => {
|
||||
const minDist = Math.min(...Utils.NoNull(distances))
|
||||
if (moveDistance > minDist) {
|
||||
|
@ -310,14 +322,14 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
|
||||
if (candidate !== undefined) {
|
||||
// We found a candidate... Search the corresponding target id:
|
||||
let targetId: number = undefined;
|
||||
let targetId: number = undefined
|
||||
let lowestDistance = Number.MAX_VALUE
|
||||
let nodeDistances = distances.get(candidate)
|
||||
for (let i = 0; i < nodeDistances.length; i++) {
|
||||
const d = nodeDistances[i]
|
||||
if (d !== undefined && d < lowestDistance) {
|
||||
lowestDistance = d;
|
||||
targetId = i;
|
||||
lowestDistance = d
|
||||
targetId = i
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -330,14 +342,14 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
closestIds[targetId] = candidate
|
||||
|
||||
// To indicate that this targetCoordinate is taken, we remove them from the distances matrix
|
||||
distances.forEach(dists => {
|
||||
distances.forEach((dists) => {
|
||||
dists[targetId] = undefined
|
||||
})
|
||||
} else {
|
||||
// Seems like all the targetCoordinates have found a source point
|
||||
unusedIds.set(candidate, {
|
||||
reason: "Unused by new way",
|
||||
hasTags: nodeInfo.get(candidate).hasTags
|
||||
hasTags: nodeInfo.get(candidate).hasTags,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -348,18 +360,21 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
distances.forEach((_, nodeId) => {
|
||||
unusedIds.set(nodeId, {
|
||||
reason: "Unused by new way",
|
||||
hasTags: nodeInfo.get(nodeId).hasTags
|
||||
hasTags: nodeInfo.get(nodeId).hasTags,
|
||||
})
|
||||
})
|
||||
|
||||
const reprojectedNodes = new Map<number, {
|
||||
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
|
||||
projectAfterIndex: number,
|
||||
distance: number,
|
||||
newLat: number,
|
||||
newLon: number,
|
||||
nodeId: number
|
||||
}>();
|
||||
const reprojectedNodes = new Map<
|
||||
number,
|
||||
{
|
||||
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
|
||||
projectAfterIndex: number
|
||||
distance: number
|
||||
newLat: number
|
||||
newLon: number
|
||||
nodeId: number
|
||||
}
|
||||
>()
|
||||
{
|
||||
// Lets check the unused ids: can they be detached or do they signify some relation with the object?
|
||||
unusedIds.forEach(({}, id) => {
|
||||
|
@ -379,36 +394,32 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
properties: {},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: self.targetCoordinates
|
||||
}
|
||||
};
|
||||
const projected = GeoOperations.nearestPoint(
|
||||
way, [node.lon, node.lat]
|
||||
)
|
||||
coordinates: self.targetCoordinates,
|
||||
},
|
||||
}
|
||||
const projected = GeoOperations.nearestPoint(way, [node.lon, node.lat])
|
||||
reprojectedNodes.set(id, {
|
||||
newLon: projected.geometry.coordinates[0],
|
||||
newLat: projected.geometry.coordinates[1],
|
||||
projectAfterIndex: projected.properties.index,
|
||||
distance: projected.properties.dist,
|
||||
nodeId: id
|
||||
nodeId: id,
|
||||
})
|
||||
})
|
||||
|
||||
reprojectedNodes.forEach((_, nodeId) => unusedIds.delete(nodeId))
|
||||
|
||||
}
|
||||
|
||||
|
||||
return {closestIds, allNodesById, osmWay, detachedNodes: unusedIds, reprojectedNodes};
|
||||
return { closestIds, allNodesById, osmWay, detachedNodes: unusedIds, reprojectedNodes }
|
||||
}
|
||||
|
||||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
const nodeDb = this.state.featurePipeline.fullNodeDatabase;
|
||||
const nodeDb = this.state.featurePipeline.fullNodeDatabase
|
||||
if (nodeDb === undefined) {
|
||||
throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
|
||||
}
|
||||
|
||||
const {closestIds, osmWay, detachedNodes, reprojectedNodes} = await this.GetClosestIds()
|
||||
const { closestIds, osmWay, detachedNodes, reprojectedNodes } = await this.GetClosestIds()
|
||||
const allChanges: ChangeDescription[] = []
|
||||
const actualIdsToUse: number[] = []
|
||||
for (let i = 0; i < closestIds.length; i++) {
|
||||
|
@ -417,47 +428,43 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
actualIdsToUse.push(actualIdsToUse[j])
|
||||
continue
|
||||
}
|
||||
const closestId = closestIds[i];
|
||||
const closestId = closestIds[i]
|
||||
const [lon, lat] = this.targetCoordinates[i]
|
||||
if (closestId === undefined) {
|
||||
|
||||
const newNodeAction = new CreateNewNodeAction(
|
||||
[],
|
||||
lat, lon,
|
||||
{
|
||||
allowReuseOfPreviouslyCreatedPoints: true,
|
||||
theme: this.theme, changeType: null
|
||||
})
|
||||
const newNodeAction = new CreateNewNodeAction([], lat, lon, {
|
||||
allowReuseOfPreviouslyCreatedPoints: true,
|
||||
theme: this.theme,
|
||||
changeType: null,
|
||||
})
|
||||
const changeDescr = await newNodeAction.CreateChangeDescriptions(changes)
|
||||
allChanges.push(...changeDescr)
|
||||
actualIdsToUse.push(newNodeAction.newElementIdNumber)
|
||||
|
||||
} else {
|
||||
const change = <ChangeDescription>{
|
||||
id: closestId,
|
||||
type: "node",
|
||||
meta: {
|
||||
theme: this.theme,
|
||||
changeType: "move"
|
||||
changeType: "move",
|
||||
},
|
||||
changes: {lon, lat}
|
||||
changes: { lon, lat },
|
||||
}
|
||||
actualIdsToUse.push(closestId)
|
||||
allChanges.push(change)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.newTags !== undefined && this.newTags.length > 0) {
|
||||
const addExtraTags = new ChangeTagAction(
|
||||
this.wayToReplaceId,
|
||||
new And(this.newTags),
|
||||
osmWay.tags, {
|
||||
osmWay.tags,
|
||||
{
|
||||
theme: this.theme,
|
||||
changeType: "conflation"
|
||||
changeType: "conflation",
|
||||
}
|
||||
)
|
||||
allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes))
|
||||
allChanges.push(...(await addExtraTags.CreateChangeDescriptions(changes)))
|
||||
}
|
||||
|
||||
const newCoordinates = [...this.targetCoordinates]
|
||||
|
@ -468,13 +475,11 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
const proj = Array.from(reprojectedNodes.values())
|
||||
proj.sort((a, b) => {
|
||||
// Sort descending
|
||||
const diff = b.projectAfterIndex - a.projectAfterIndex;
|
||||
const diff = b.projectAfterIndex - a.projectAfterIndex
|
||||
if (diff !== 0) {
|
||||
return diff
|
||||
}
|
||||
return b.distance - a.distance;
|
||||
|
||||
|
||||
return b.distance - a.distance
|
||||
})
|
||||
|
||||
for (const reprojectedNode of proj) {
|
||||
|
@ -483,13 +488,20 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
type: "node",
|
||||
meta: {
|
||||
theme: this.theme,
|
||||
changeType: "move"
|
||||
changeType: "move",
|
||||
},
|
||||
changes: {lon: reprojectedNode.newLon, lat: reprojectedNode.newLat}
|
||||
changes: { lon: reprojectedNode.newLon, lat: reprojectedNode.newLat },
|
||||
}
|
||||
allChanges.push(change)
|
||||
actualIdsToUse.splice(reprojectedNode.projectAfterIndex + 1, 0, reprojectedNode.nodeId)
|
||||
newCoordinates.splice(reprojectedNode.projectAfterIndex + 1, 0, [reprojectedNode.newLon, reprojectedNode.newLat])
|
||||
actualIdsToUse.splice(
|
||||
reprojectedNode.projectAfterIndex + 1,
|
||||
0,
|
||||
reprojectedNode.nodeId
|
||||
)
|
||||
newCoordinates.splice(reprojectedNode.projectAfterIndex + 1, 0, [
|
||||
reprojectedNode.newLon,
|
||||
reprojectedNode.newLat,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -499,42 +511,46 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
id: osmWay.id,
|
||||
changes: {
|
||||
nodes: actualIdsToUse,
|
||||
coordinates: newCoordinates
|
||||
coordinates: newCoordinates,
|
||||
},
|
||||
meta: {
|
||||
theme: this.theme,
|
||||
changeType: "conflation"
|
||||
}
|
||||
changeType: "conflation",
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// Some nodes might need to be deleted
|
||||
if (detachedNodes.size > 0) {
|
||||
detachedNodes.forEach(({hasTags, reason}, nodeId) => {
|
||||
detachedNodes.forEach(({ hasTags, reason }, nodeId) => {
|
||||
const parentWays = nodeDb.GetParentWays(nodeId)
|
||||
const index = parentWays.data.map(w => w.id).indexOf(osmWay.id)
|
||||
const index = parentWays.data.map((w) => w.id).indexOf(osmWay.id)
|
||||
if (index < 0) {
|
||||
console.error("ReplaceGeometryAction is trying to detach node " + nodeId + ", but it isn't listed as being part of way " + osmWay.id)
|
||||
return;
|
||||
console.error(
|
||||
"ReplaceGeometryAction is trying to detach node " +
|
||||
nodeId +
|
||||
", but it isn't listed as being part of way " +
|
||||
osmWay.id
|
||||
)
|
||||
return
|
||||
}
|
||||
// We detachted this node - so we unregister
|
||||
parentWays.data.splice(index, 1)
|
||||
parentWays.ping();
|
||||
parentWays.ping()
|
||||
|
||||
if (hasTags) {
|
||||
// Has tags: we leave this node alone
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (parentWays.data.length != 0) {
|
||||
// Still part of other ways: we leave this node alone!
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Removing node " + nodeId, "as it isn't needed anymore by any way")
|
||||
allChanges.push({
|
||||
meta: {
|
||||
theme: this.theme,
|
||||
changeType: "delete"
|
||||
changeType: "delete",
|
||||
},
|
||||
doDelete: true,
|
||||
type: "node",
|
||||
|
@ -545,6 +561,4 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
|
||||
return allChanges
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
import {OsmObject, OsmWay} from "../OsmObject";
|
||||
import {Changes} from "../Changes";
|
||||
import {GeoOperations} from "../../GeoOperations";
|
||||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import RelationSplitHandler from "./RelationSplitHandler";
|
||||
import { OsmObject, OsmWay } from "../OsmObject"
|
||||
import { Changes } from "../Changes"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
import OsmChangeAction from "./OsmChangeAction"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import RelationSplitHandler from "./RelationSplitHandler"
|
||||
|
||||
interface SplitInfo {
|
||||
originalIndex?: number, // or negative for new elements
|
||||
lngLat: [number, number],
|
||||
originalIndex?: number // or negative for new elements
|
||||
lngLat: [number, number]
|
||||
doSplit: boolean
|
||||
}
|
||||
|
||||
export default class SplitAction extends OsmChangeAction {
|
||||
private readonly wayId: string;
|
||||
private readonly _splitPointsCoordinates: [number, number] []// lon, lat
|
||||
private _meta: { theme: string, changeType: "split" };
|
||||
private _toleranceInMeters: number;
|
||||
private readonly wayId: string
|
||||
private readonly _splitPointsCoordinates: [number, number][] // lon, lat
|
||||
private _meta: { theme: string; changeType: "split" }
|
||||
private _toleranceInMeters: number
|
||||
|
||||
/**
|
||||
* Create a changedescription for splitting a point.
|
||||
|
@ -25,12 +25,17 @@ export default class SplitAction extends OsmChangeAction {
|
|||
* @param meta
|
||||
* @param toleranceInMeters: if a splitpoint closer then this amount of meters to an existing point, the existing point will be used to split the line instead of a new point
|
||||
*/
|
||||
constructor(wayId: string, splitPointCoordinates: [number, number][], meta: { theme: string }, toleranceInMeters = 5) {
|
||||
constructor(
|
||||
wayId: string,
|
||||
splitPointCoordinates: [number, number][],
|
||||
meta: { theme: string },
|
||||
toleranceInMeters = 5
|
||||
) {
|
||||
super(wayId, true)
|
||||
this.wayId = wayId;
|
||||
this.wayId = wayId
|
||||
this._splitPointsCoordinates = splitPointCoordinates
|
||||
this._toleranceInMeters = toleranceInMeters;
|
||||
this._meta = {...meta, changeType: "split"};
|
||||
this._toleranceInMeters = toleranceInMeters
|
||||
this._meta = { ...meta, changeType: "split" }
|
||||
}
|
||||
|
||||
private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] {
|
||||
|
@ -47,16 +52,16 @@ export default class SplitAction extends OsmChangeAction {
|
|||
}
|
||||
}
|
||||
wayParts.push(currentPart)
|
||||
return wayParts.filter(wp => wp.length > 0)
|
||||
return wayParts.filter((wp) => wp.length > 0)
|
||||
}
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
const originalElement = <OsmWay>await OsmObject.DownloadObjectAsync(this.wayId)
|
||||
const originalNodes = originalElement.nodes;
|
||||
const originalNodes = originalElement.nodes
|
||||
|
||||
// First, calculate splitpoints and remove points close to one another
|
||||
const splitInfo = this.CalculateSplitCoordinates(originalElement, this._toleranceInMeters)
|
||||
// Now we have a list with e.g.
|
||||
// Now we have a list with e.g.
|
||||
// [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}]
|
||||
|
||||
// Lets change 'originalIndex' to the actual node id first (or assign a new id if needed):
|
||||
|
@ -64,19 +69,19 @@ export default class SplitAction extends OsmChangeAction {
|
|||
if (element.originalIndex >= 0) {
|
||||
element.originalIndex = originalElement.nodes[element.originalIndex]
|
||||
} else {
|
||||
element.originalIndex = changes.getNewID();
|
||||
element.originalIndex = changes.getNewID()
|
||||
}
|
||||
}
|
||||
|
||||
// Next up is creating actual parts from this
|
||||
const wayParts: SplitInfo[][] = SplitAction.SegmentSplitInfo(splitInfo);
|
||||
const wayParts: SplitInfo[][] = SplitAction.SegmentSplitInfo(splitInfo)
|
||||
// Allright! At this point, we have our new ways!
|
||||
// Which one is the longest of them (and can keep the id)?
|
||||
|
||||
let longest = undefined;
|
||||
let longest = undefined
|
||||
for (const wayPart of wayParts) {
|
||||
if (longest === undefined) {
|
||||
longest = wayPart;
|
||||
longest = wayPart
|
||||
continue
|
||||
}
|
||||
if (wayPart.length > longest.length) {
|
||||
|
@ -88,16 +93,16 @@ export default class SplitAction extends OsmChangeAction {
|
|||
// Let's create the new points as needed
|
||||
for (const element of splitInfo) {
|
||||
if (element.originalIndex >= 0) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
changeDescription.push({
|
||||
type: "node",
|
||||
id: element.originalIndex,
|
||||
changes: {
|
||||
lon: element.lngLat[0],
|
||||
lat: element.lngLat[1]
|
||||
lat: element.lngLat[1],
|
||||
},
|
||||
meta: this._meta
|
||||
meta: this._meta,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -107,24 +112,23 @@ export default class SplitAction extends OsmChangeAction {
|
|||
const allWaysNodesInOrder: number[][] = []
|
||||
// Lets create OsmWays based on them
|
||||
for (const wayPart of wayParts) {
|
||||
|
||||
let isOriginal = wayPart === longest
|
||||
if (isOriginal) {
|
||||
// We change the actual element!
|
||||
const nodeIds = wayPart.map(p => p.originalIndex)
|
||||
const nodeIds = wayPart.map((p) => p.originalIndex)
|
||||
changeDescription.push({
|
||||
type: "way",
|
||||
id: originalElement.id,
|
||||
changes: {
|
||||
coordinates: wayPart.map(p => p.lngLat),
|
||||
nodes: nodeIds
|
||||
coordinates: wayPart.map((p) => p.lngLat),
|
||||
nodes: nodeIds,
|
||||
},
|
||||
meta: this._meta
|
||||
meta: this._meta,
|
||||
})
|
||||
allWayIdsInOrder.push(originalElement.id)
|
||||
allWaysNodesInOrder.push(nodeIds)
|
||||
} else {
|
||||
let id = changes.getNewID();
|
||||
let id = changes.getNewID()
|
||||
// Copy the tags from the original object onto the new
|
||||
const kv = []
|
||||
for (const k in originalElement.tags) {
|
||||
|
@ -132,20 +136,20 @@ export default class SplitAction extends OsmChangeAction {
|
|||
continue
|
||||
}
|
||||
if (k.startsWith("_") || k === "id") {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
kv.push({k: k, v: originalElement.tags[k]})
|
||||
kv.push({ k: k, v: originalElement.tags[k] })
|
||||
}
|
||||
const nodeIds = wayPart.map(p => p.originalIndex)
|
||||
const nodeIds = wayPart.map((p) => p.originalIndex)
|
||||
changeDescription.push({
|
||||
type: "way",
|
||||
id: id,
|
||||
tags: kv,
|
||||
changes: {
|
||||
coordinates: wayPart.map(p => p.lngLat),
|
||||
nodes: nodeIds
|
||||
coordinates: wayPart.map((p) => p.lngLat),
|
||||
nodes: nodeIds,
|
||||
},
|
||||
meta: this._meta
|
||||
meta: this._meta,
|
||||
})
|
||||
|
||||
allWayIdsInOrder.push(id)
|
||||
|
@ -157,13 +161,16 @@ export default class SplitAction extends OsmChangeAction {
|
|||
// At least, the order of the ways is identical, so we can keep the same roles
|
||||
const relations = await OsmObject.DownloadReferencingRelations(this.wayId)
|
||||
for (const relation of relations) {
|
||||
const changDescrs = await new RelationSplitHandler({
|
||||
relation: relation,
|
||||
allWayIdsInOrder: allWayIdsInOrder,
|
||||
originalNodes: originalNodes,
|
||||
allWaysNodesInOrder: allWaysNodesInOrder,
|
||||
originalWayId: originalElement.id,
|
||||
}, this._meta.theme).CreateChangeDescriptions(changes)
|
||||
const changDescrs = await new RelationSplitHandler(
|
||||
{
|
||||
relation: relation,
|
||||
allWayIdsInOrder: allWayIdsInOrder,
|
||||
originalNodes: originalNodes,
|
||||
allWaysNodesInOrder: allWaysNodesInOrder,
|
||||
originalWayId: originalElement.id,
|
||||
},
|
||||
this._meta.theme
|
||||
).CreateChangeDescriptions(changes)
|
||||
changeDescription.push(...changDescrs)
|
||||
}
|
||||
|
||||
|
@ -180,48 +187,47 @@ export default class SplitAction extends OsmChangeAction {
|
|||
private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] {
|
||||
const wayGeoJson = osmWay.asGeoJson()
|
||||
// Should be [lon, lat][]
|
||||
const originalPoints: [number, number][] = osmWay.coordinates.map(c => [c[1], c[0]])
|
||||
const originalPoints: [number, number][] = osmWay.coordinates.map((c) => [c[1], c[0]])
|
||||
const allPoints: {
|
||||
// lon, lat
|
||||
coordinates: [number, number],
|
||||
isSplitPoint: boolean,
|
||||
originalIndex?: number, // Original index
|
||||
dist: number, // Distance from the nearest point on the original line
|
||||
coordinates: [number, number]
|
||||
isSplitPoint: boolean
|
||||
originalIndex?: number // Original index
|
||||
dist: number // Distance from the nearest point on the original line
|
||||
location: number // Distance from the start of the way
|
||||
}[] = this._splitPointsCoordinates.map(c => {
|
||||
}[] = this._splitPointsCoordinates.map((c) => {
|
||||
// From the turf.js docs:
|
||||
// The properties object will contain three values:
|
||||
// The properties object will contain three values:
|
||||
// - `index`: closest point was found on nth line part,
|
||||
// - `dist`: distance between pt and the closest point,
|
||||
// - `dist`: distance between pt and the closest point,
|
||||
// `location`: distance along the line between start and the closest point.
|
||||
let projected = GeoOperations.nearestPoint(wayGeoJson, c)
|
||||
// c is lon lat
|
||||
return ({
|
||||
return {
|
||||
coordinates: c,
|
||||
isSplitPoint: true,
|
||||
dist: projected.properties.dist,
|
||||
location: projected.properties.location
|
||||
});
|
||||
location: projected.properties.location,
|
||||
}
|
||||
})
|
||||
|
||||
// We have a bunch of coordinates here: [ [lon, lon], [lat, lon], ...] ...
|
||||
// We project them onto the line (which should yield pretty much the same point and add them to allPoints
|
||||
for (let i = 0; i < originalPoints.length; i++) {
|
||||
let originalPoint = originalPoints[i];
|
||||
let originalPoint = originalPoints[i]
|
||||
let projected = GeoOperations.nearestPoint(wayGeoJson, originalPoint)
|
||||
allPoints.push({
|
||||
coordinates: originalPoint,
|
||||
isSplitPoint: false,
|
||||
location: projected.properties.location,
|
||||
originalIndex: i,
|
||||
dist: projected.properties.dist
|
||||
dist: projected.properties.dist,
|
||||
})
|
||||
}
|
||||
// At this point, we have a list of both the split point and the old points, with some properties to discriminate between them
|
||||
// We sort this list so that the new points are at the same location
|
||||
allPoints.sort((a, b) => a.location - b.location)
|
||||
|
||||
|
||||
for (let i = allPoints.length - 2; i >= 1; i--) {
|
||||
// We 'merge' points with already existing nodes if they are close enough to avoid closeby elements
|
||||
|
||||
|
@ -244,7 +250,7 @@ export default class SplitAction extends OsmChangeAction {
|
|||
|
||||
if (distToNext * 1000 > toleranceInM && distToPrev * 1000 > toleranceInM) {
|
||||
// Both are too far away to mark them as the split point
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
let closest = nextPoint
|
||||
|
@ -256,9 +262,8 @@ export default class SplitAction extends OsmChangeAction {
|
|||
// We can not split on the first or last points...
|
||||
continue
|
||||
}
|
||||
closest.isSplitPoint = true;
|
||||
closest.isSplitPoint = true
|
||||
allPoints.splice(i, 1)
|
||||
|
||||
}
|
||||
|
||||
const splitInfo: SplitInfo[] = []
|
||||
|
@ -267,19 +272,17 @@ export default class SplitAction extends OsmChangeAction {
|
|||
for (const p of allPoints) {
|
||||
let index = p.originalIndex
|
||||
if (index === undefined) {
|
||||
index = nextId;
|
||||
nextId--;
|
||||
index = nextId
|
||||
nextId--
|
||||
}
|
||||
const splitInfoElement = {
|
||||
originalIndex: index,
|
||||
lngLat: p.coordinates,
|
||||
doSplit: p.isSplitPoint
|
||||
doSplit: p.isSplitPoint,
|
||||
}
|
||||
splitInfo.push(splitInfoElement)
|
||||
}
|
||||
|
||||
return splitInfo
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,106 +1,110 @@
|
|||
import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import Constants from "../../Models/Constants";
|
||||
import OsmChangeAction from "./Actions/OsmChangeAction";
|
||||
import {ChangeDescription, ChangeDescriptionTools} from "./Actions/ChangeDescription";
|
||||
import {Utils} from "../../Utils";
|
||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||
import SimpleMetaTagger from "../SimpleMetaTagger";
|
||||
import CreateNewNodeAction from "./Actions/CreateNewNodeAction";
|
||||
import FeatureSource from "../FeatureSource/FeatureSource";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import {GeoLocationPointProperties} from "../Actors/GeoLocationHandler";
|
||||
import {GeoOperations} from "../GeoOperations";
|
||||
import {ChangesetHandler, ChangesetTag} from "./ChangesetHandler";
|
||||
import {OsmConnection} from "./OsmConnection";
|
||||
import { OsmNode, OsmObject, OsmRelation, OsmWay } from "./OsmObject"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import Constants from "../../Models/Constants"
|
||||
import OsmChangeAction from "./Actions/OsmChangeAction"
|
||||
import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescription"
|
||||
import { Utils } from "../../Utils"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import SimpleMetaTagger from "../SimpleMetaTagger"
|
||||
import CreateNewNodeAction from "./Actions/CreateNewNodeAction"
|
||||
import FeatureSource from "../FeatureSource/FeatureSource"
|
||||
import { ElementStorage } from "../ElementStorage"
|
||||
import { GeoLocationPointProperties } from "../Actors/GeoLocationHandler"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler"
|
||||
import { OsmConnection } from "./OsmConnection"
|
||||
|
||||
/**
|
||||
* Handles all changes made to OSM.
|
||||
* Needs an authenticator via OsmConnection
|
||||
*/
|
||||
export class Changes {
|
||||
|
||||
public readonly name = "Newly added features"
|
||||
/**
|
||||
* All the newly created features as featureSource + all the modified features
|
||||
*/
|
||||
public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
|
||||
public readonly pendingChanges: UIEventSource<ChangeDescription[]> = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
||||
public features = new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
public readonly pendingChanges: UIEventSource<ChangeDescription[]> =
|
||||
LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
||||
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
|
||||
public readonly state: { allElements: ElementStorage; osmConnection: OsmConnection }
|
||||
public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined)
|
||||
|
||||
|
||||
private historicalUserLocations: FeatureSource
|
||||
private _nextId: number = -1; // Newly assigned ID's are negative
|
||||
private readonly isUploading = new UIEventSource(false);
|
||||
private _nextId: number = -1 // Newly assigned ID's are negative
|
||||
private readonly isUploading = new UIEventSource(false)
|
||||
private readonly previouslyCreated: OsmObject[] = []
|
||||
private readonly _leftRightSensitive: boolean;
|
||||
private _changesetHandler: ChangesetHandler;
|
||||
private readonly _leftRightSensitive: boolean
|
||||
private _changesetHandler: ChangesetHandler
|
||||
|
||||
constructor(
|
||||
state?: {
|
||||
allElements: ElementStorage,
|
||||
allElements: ElementStorage
|
||||
osmConnection: OsmConnection
|
||||
},
|
||||
leftRightSensitive: boolean = false) {
|
||||
this._leftRightSensitive = leftRightSensitive;
|
||||
leftRightSensitive: boolean = false
|
||||
) {
|
||||
this._leftRightSensitive = leftRightSensitive
|
||||
// We keep track of all changes just as well
|
||||
this.allChanges.setData([...this.pendingChanges.data])
|
||||
// If a pending change contains a negative ID, we save that
|
||||
this._nextId = Math.min(-1, ...this.pendingChanges.data?.map(pch => pch.id) ?? [])
|
||||
this.state = state;
|
||||
this._changesetHandler = state?.osmConnection?.CreateChangesetHandler(state.allElements, this)
|
||||
this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id) ?? []))
|
||||
this.state = state
|
||||
this._changesetHandler = state?.osmConnection?.CreateChangesetHandler(
|
||||
state.allElements,
|
||||
this
|
||||
)
|
||||
|
||||
// Note: a changeset might be reused which was opened just before and might have already used some ids
|
||||
// This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
|
||||
}
|
||||
|
||||
static createChangesetFor(csId: string,
|
||||
allChanges: {
|
||||
modifiedObjects: OsmObject[],
|
||||
newObjects: OsmObject[],
|
||||
deletedObjects: OsmObject[]
|
||||
}): string {
|
||||
|
||||
static createChangesetFor(
|
||||
csId: string,
|
||||
allChanges: {
|
||||
modifiedObjects: OsmObject[]
|
||||
newObjects: OsmObject[]
|
||||
deletedObjects: OsmObject[]
|
||||
}
|
||||
): string {
|
||||
const changedElements = allChanges.modifiedObjects ?? []
|
||||
const newElements = allChanges.newObjects ?? []
|
||||
const deletedElements = allChanges.deletedObjects ?? []
|
||||
|
||||
let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`;
|
||||
let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`
|
||||
if (newElements.length > 0) {
|
||||
changes +=
|
||||
"\n<create>\n" +
|
||||
newElements.map(e => e.ChangesetXML(csId)).join("\n") +
|
||||
"</create>";
|
||||
newElements.map((e) => e.ChangesetXML(csId)).join("\n") +
|
||||
"</create>"
|
||||
}
|
||||
if (changedElements.length > 0) {
|
||||
changes +=
|
||||
"\n<modify>\n" +
|
||||
changedElements.map(e => e.ChangesetXML(csId)).join("\n") +
|
||||
"\n</modify>";
|
||||
changedElements.map((e) => e.ChangesetXML(csId)).join("\n") +
|
||||
"\n</modify>"
|
||||
}
|
||||
|
||||
if (deletedElements.length > 0) {
|
||||
changes +=
|
||||
"\n<delete>\n" +
|
||||
deletedElements.map(e => e.ChangesetXML(csId)).join("\n") +
|
||||
deletedElements.map((e) => e.ChangesetXML(csId)).join("\n") +
|
||||
"\n</delete>"
|
||||
}
|
||||
|
||||
changes += "</osmChange>";
|
||||
return changes;
|
||||
changes += "</osmChange>"
|
||||
return changes
|
||||
}
|
||||
|
||||
private static GetNeededIds(changes: ChangeDescription[]) {
|
||||
return Utils.Dedup(changes.filter(c => c.id >= 0)
|
||||
.map(c => c.type + "/" + c.id))
|
||||
return Utils.Dedup(changes.filter((c) => c.id >= 0).map((c) => c.type + "/" + c.id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new ID and updates the value for the next ID
|
||||
*/
|
||||
public getNewID() {
|
||||
return this._nextId--;
|
||||
return this._nextId--
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -109,64 +113,71 @@ export class Changes {
|
|||
*/
|
||||
public async flushChanges(flushreason: string = undefined): Promise<void> {
|
||||
if (this.pendingChanges.data.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (this.isUploading.data) {
|
||||
console.log("Is already uploading... Abort")
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
console.log("Uploading changes due to: ", flushreason)
|
||||
this.isUploading.setData(true)
|
||||
try {
|
||||
const csNumber = await this.flushChangesAsync()
|
||||
this.isUploading.setData(false)
|
||||
console.log("Changes flushed. Your changeset is " + csNumber);
|
||||
console.log("Changes flushed. Your changeset is " + csNumber)
|
||||
} catch (e) {
|
||||
this.isUploading.setData(false)
|
||||
console.error("Flushing changes failed due to", e);
|
||||
console.error("Flushing changes failed due to", e)
|
||||
}
|
||||
}
|
||||
|
||||
public async applyAction(action: OsmChangeAction): Promise<void> {
|
||||
const changeDescriptions = await action.Perform(this)
|
||||
changeDescriptions[0].meta.distanceToObject = this.calculateDistanceToChanges(action, changeDescriptions)
|
||||
changeDescriptions[0].meta.distanceToObject = this.calculateDistanceToChanges(
|
||||
action,
|
||||
changeDescriptions
|
||||
)
|
||||
this.applyChanges(changeDescriptions)
|
||||
}
|
||||
|
||||
public applyChanges(changes: ChangeDescription[]) {
|
||||
console.log("Received changes:", changes)
|
||||
this.pendingChanges.data.push(...changes);
|
||||
this.pendingChanges.ping();
|
||||
this.pendingChanges.data.push(...changes)
|
||||
this.pendingChanges.ping()
|
||||
this.allChanges.data.push(...changes)
|
||||
this.allChanges.ping()
|
||||
}
|
||||
|
||||
private calculateDistanceToChanges(change: OsmChangeAction, changeDescriptions: ChangeDescription[]) {
|
||||
|
||||
private calculateDistanceToChanges(
|
||||
change: OsmChangeAction,
|
||||
changeDescriptions: ChangeDescription[]
|
||||
) {
|
||||
const locations = this.historicalUserLocations?.features?.data
|
||||
if (locations === undefined) {
|
||||
// No state loaded or no locations -> we can't calculate...
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (!change.trackStatistics) {
|
||||
// Probably irrelevant, such as a new helper node
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const recentLocationPoints = locations.map(ff => ff.feature)
|
||||
.filter(feat => feat.geometry.type === "Point")
|
||||
.filter(feat => {
|
||||
const visitTime = new Date((<GeoLocationPointProperties><any>feat.properties).date)
|
||||
const recentLocationPoints = locations
|
||||
.map((ff) => ff.feature)
|
||||
.filter((feat) => feat.geometry.type === "Point")
|
||||
.filter((feat) => {
|
||||
const visitTime = new Date(
|
||||
(<GeoLocationPointProperties>(<any>feat.properties)).date
|
||||
)
|
||||
// In seconds
|
||||
const diff = (now.getTime() - visitTime.getTime()) / 1000
|
||||
return diff < Constants.nearbyVisitTime;
|
||||
return diff < Constants.nearbyVisitTime
|
||||
})
|
||||
if (recentLocationPoints.length === 0) {
|
||||
// Probably no GPS enabled/no fix
|
||||
return;
|
||||
// Probably no GPS enabled/no fix
|
||||
return
|
||||
}
|
||||
|
||||
// The applicable points, contain information in their properties about location, time and GPS accuracy
|
||||
|
@ -182,7 +193,10 @@ export class Changes {
|
|||
}
|
||||
|
||||
for (const changeDescription of changeDescriptions) {
|
||||
const chng: { lat: number, lon: number } | { coordinates: [number, number][] } | { members } = changeDescription.changes
|
||||
const chng:
|
||||
| { lat: number; lon: number }
|
||||
| { coordinates: [number, number][] }
|
||||
| { members } = changeDescription.changes
|
||||
if (chng === undefined) {
|
||||
continue
|
||||
}
|
||||
|
@ -194,61 +208,85 @@ export class Changes {
|
|||
}
|
||||
}
|
||||
|
||||
return Math.min(...changedObjectCoordinates.map(coor =>
|
||||
Math.min(...recentLocationPoints.map(gpsPoint => {
|
||||
const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint)
|
||||
return GeoOperations.distanceBetween(coor, otherCoor)
|
||||
}))
|
||||
))
|
||||
return Math.min(
|
||||
...changedObjectCoordinates.map((coor) =>
|
||||
Math.min(
|
||||
...recentLocationPoints.map((gpsPoint) => {
|
||||
const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint)
|
||||
return GeoOperations.distanceBetween(coor, otherCoor)
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* UPload the selected changes to OSM.
|
||||
* Returns 'true' if successfull and if they can be removed
|
||||
*/
|
||||
private async flushSelectChanges(pending: ChangeDescription[], openChangeset: UIEventSource<number>): Promise<boolean> {
|
||||
const self = this;
|
||||
private async flushSelectChanges(
|
||||
pending: ChangeDescription[],
|
||||
openChangeset: UIEventSource<number>
|
||||
): Promise<boolean> {
|
||||
const self = this
|
||||
const neededIds = Changes.GetNeededIds(pending)
|
||||
|
||||
const osmObjects = Utils.NoNull(await Promise.all(neededIds.map(async id =>
|
||||
OsmObject.DownloadObjectAsync(id).catch(e => {
|
||||
console.error("Could not download OSM-object", id, " dropping it from the changes ("+e+")")
|
||||
pending = pending.filter(ch => ch.type + "/" + ch.id !== id)
|
||||
return undefined;
|
||||
}))));
|
||||
const osmObjects = Utils.NoNull(
|
||||
await Promise.all(
|
||||
neededIds.map(async (id) =>
|
||||
OsmObject.DownloadObjectAsync(id).catch((e) => {
|
||||
console.error(
|
||||
"Could not download OSM-object",
|
||||
id,
|
||||
" dropping it from the changes (" + e + ")"
|
||||
)
|
||||
pending = pending.filter((ch) => ch.type + "/" + ch.id !== id)
|
||||
return undefined
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (this._leftRightSensitive) {
|
||||
osmObjects.forEach(obj => SimpleMetaTagger.removeBothTagging(obj.tags))
|
||||
osmObjects.forEach((obj) => SimpleMetaTagger.removeBothTagging(obj.tags))
|
||||
}
|
||||
|
||||
console.log("Got the fresh objects!", osmObjects, "pending: ", pending)
|
||||
if(pending.length == 0){
|
||||
if (pending.length == 0) {
|
||||
console.log("No pending changes...")
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
const perType = Array.from(
|
||||
Utils.Hist(pending.filter(descr => descr.meta.changeType !== undefined && descr.meta.changeType !== null)
|
||||
.map(descr => descr.meta.changeType)), ([key, count]) => (
|
||||
{
|
||||
key: key,
|
||||
value: count,
|
||||
aggregate: true
|
||||
}))
|
||||
const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined)
|
||||
.map(descr => ({
|
||||
Utils.Hist(
|
||||
pending
|
||||
.filter(
|
||||
(descr) =>
|
||||
descr.meta.changeType !== undefined && descr.meta.changeType !== null
|
||||
)
|
||||
.map((descr) => descr.meta.changeType)
|
||||
),
|
||||
([key, count]) => ({
|
||||
key: key,
|
||||
value: count,
|
||||
aggregate: true,
|
||||
})
|
||||
)
|
||||
const motivations = pending
|
||||
.filter((descr) => descr.meta.specialMotivation !== undefined)
|
||||
.map((descr) => ({
|
||||
key: descr.meta.changeType + ":" + descr.type + "/" + descr.id,
|
||||
value: descr.meta.specialMotivation
|
||||
value: descr.meta.specialMotivation,
|
||||
}))
|
||||
|
||||
const distances = Utils.NoNull(pending.map(descr => descr.meta.distanceToObject));
|
||||
const distances = Utils.NoNull(pending.map((descr) => descr.meta.distanceToObject))
|
||||
distances.sort((a, b) => a - b)
|
||||
const perBinCount = Constants.distanceToChangeObjectBins.map(_ => 0)
|
||||
const perBinCount = Constants.distanceToChangeObjectBins.map((_) => 0)
|
||||
|
||||
let j = 0;
|
||||
let j = 0
|
||||
const maxDistances = Constants.distanceToChangeObjectBins
|
||||
for (let i = 0; i < maxDistances.length; i++) {
|
||||
const maxDistance = maxDistances[i];
|
||||
const maxDistance = maxDistances[i]
|
||||
// distances is sorted in ascending order, so as soon as one is to big, all the resting elements will be bigger too
|
||||
while (j < distances.length && distances[j] < maxDistance) {
|
||||
perBinCount[i]++
|
||||
|
@ -256,21 +294,23 @@ export class Changes {
|
|||
}
|
||||
}
|
||||
|
||||
const perBinMessage = Utils.NoNull(perBinCount.map((count, i) => {
|
||||
if (count === 0) {
|
||||
return undefined
|
||||
}
|
||||
const maxD = maxDistances[i]
|
||||
let key = `change_within_${maxD}m`
|
||||
if (maxD === Number.MAX_VALUE) {
|
||||
key = `change_over_${maxDistances[i - 1]}m`
|
||||
}
|
||||
return {
|
||||
key,
|
||||
value: count,
|
||||
aggregate: true
|
||||
}
|
||||
}))
|
||||
const perBinMessage = Utils.NoNull(
|
||||
perBinCount.map((count, i) => {
|
||||
if (count === 0) {
|
||||
return undefined
|
||||
}
|
||||
const maxD = maxDistances[i]
|
||||
let key = `change_within_${maxD}m`
|
||||
if (maxD === Number.MAX_VALUE) {
|
||||
key = `change_over_${maxDistances[i - 1]}m`
|
||||
}
|
||||
return {
|
||||
key,
|
||||
value: count,
|
||||
aggregate: true,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// This method is only called with changedescriptions for this theme
|
||||
const theme = pending[0].meta.theme
|
||||
|
@ -279,46 +319,47 @@ export class Changes {
|
|||
comment += "\n\n" + this.extraComment.data
|
||||
}
|
||||
|
||||
const metatags: ChangesetTag[] = [{
|
||||
key: "comment",
|
||||
value: comment
|
||||
},
|
||||
const metatags: ChangesetTag[] = [
|
||||
{
|
||||
key: "comment",
|
||||
value: comment,
|
||||
},
|
||||
{
|
||||
key: "theme",
|
||||
value: theme
|
||||
value: theme,
|
||||
},
|
||||
...perType,
|
||||
...motivations,
|
||||
...perBinMessage
|
||||
...perBinMessage,
|
||||
]
|
||||
|
||||
await this._changesetHandler.UploadChangeset(
|
||||
(csId, remappings) =>{
|
||||
if(remappings.size > 0){
|
||||
(csId, remappings) => {
|
||||
if (remappings.size > 0) {
|
||||
console.log("Rewriting pending changes from", pending, "with", remappings)
|
||||
pending = pending.map(ch => ChangeDescriptionTools.rewriteIds(ch, remappings))
|
||||
pending = pending.map((ch) => ChangeDescriptionTools.rewriteIds(ch, remappings))
|
||||
console.log("Result is", pending)
|
||||
}
|
||||
const changes: {
|
||||
newObjects: OsmObject[],
|
||||
newObjects: OsmObject[]
|
||||
modifiedObjects: OsmObject[]
|
||||
deletedObjects: OsmObject[]
|
||||
} = self.CreateChangesetObjects(pending, osmObjects)
|
||||
return Changes.createChangesetFor("" + csId, changes)
|
||||
return Changes.createChangesetFor("" + csId, changes)
|
||||
},
|
||||
metatags,
|
||||
openChangeset
|
||||
)
|
||||
|
||||
console.log("Upload successfull!")
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
private async flushChangesAsync(): Promise<void> {
|
||||
const self = this;
|
||||
const self = this
|
||||
try {
|
||||
// At last, we build the changeset and upload
|
||||
const pending = self.pendingChanges.data;
|
||||
const pending = self.pendingChanges.data
|
||||
|
||||
const pendingPerTheme = new Map<string, ChangeDescription[]>()
|
||||
for (const changeDescription of pending) {
|
||||
|
@ -329,50 +370,62 @@ export class Changes {
|
|||
pendingPerTheme.get(theme).push(changeDescription)
|
||||
}
|
||||
|
||||
const successes = await Promise.all(Array.from(pendingPerTheme,
|
||||
async ([theme, pendingChanges]) => {
|
||||
const successes = await Promise.all(
|
||||
Array.from(pendingPerTheme, async ([theme, pendingChanges]) => {
|
||||
try {
|
||||
const openChangeset = this.state.osmConnection.GetPreference("current-open-changeset-" + theme).sync(
|
||||
str => {
|
||||
const n = Number(str);
|
||||
if (isNaN(n)) {
|
||||
return undefined
|
||||
}
|
||||
return n
|
||||
}, [], n => "" + n
|
||||
);
|
||||
console.log("Using current-open-changeset-" + theme + " from the preferences, got " + openChangeset.data)
|
||||
const openChangeset = this.state.osmConnection
|
||||
.GetPreference("current-open-changeset-" + theme)
|
||||
.sync(
|
||||
(str) => {
|
||||
const n = Number(str)
|
||||
if (isNaN(n)) {
|
||||
return undefined
|
||||
}
|
||||
return n
|
||||
},
|
||||
[],
|
||||
(n) => "" + n
|
||||
)
|
||||
console.log(
|
||||
"Using current-open-changeset-" +
|
||||
theme +
|
||||
" from the preferences, got " +
|
||||
openChangeset.data
|
||||
)
|
||||
|
||||
return await self.flushSelectChanges(pendingChanges, openChangeset);
|
||||
return await self.flushSelectChanges(pendingChanges, openChangeset)
|
||||
} catch (e) {
|
||||
console.error("Could not upload some changes:", e)
|
||||
return false
|
||||
}
|
||||
}))
|
||||
})
|
||||
)
|
||||
|
||||
if (!successes.some(s => s == false)) {
|
||||
if (!successes.some((s) => s == false)) {
|
||||
// All changes successfull, we clear the data!
|
||||
this.pendingChanges.setData([]);
|
||||
this.pendingChanges.setData([])
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e)
|
||||
console.error(
|
||||
"Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those",
|
||||
e
|
||||
)
|
||||
self.pendingChanges.setData([])
|
||||
} finally {
|
||||
self.isUploading.setData(false)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): {
|
||||
newObjects: OsmObject[],
|
||||
public CreateChangesetObjects(
|
||||
changes: ChangeDescription[],
|
||||
downloadedOsmObjects: OsmObject[]
|
||||
): {
|
||||
newObjects: OsmObject[]
|
||||
modifiedObjects: OsmObject[]
|
||||
deletedObjects: OsmObject[]
|
||||
|
||||
} {
|
||||
const objects: Map<string, OsmObject> = new Map<string, OsmObject>()
|
||||
const states: Map<string, "unchanged" | "created" | "modified" | "deleted"> = new Map();
|
||||
const states: Map<string, "unchanged" | "created" | "modified" | "deleted"> = new Map()
|
||||
|
||||
for (const o of downloadedOsmObjects) {
|
||||
objects.set(o.type + "/" + o.id, o)
|
||||
|
@ -385,7 +438,7 @@ export class Changes {
|
|||
}
|
||||
|
||||
for (const change of changes) {
|
||||
let changed = false;
|
||||
let changed = false
|
||||
const id = change.type + "/" + change.id
|
||||
if (!objects.has(id)) {
|
||||
// The object hasn't been seen before, so it doesn't exist yet and is newly created by its very definition
|
||||
|
@ -400,24 +453,24 @@ export class Changes {
|
|||
// This is a new object that should be created
|
||||
states.set(id, "created")
|
||||
console.log("Creating object for changeDescription", change)
|
||||
let osmObj: OsmObject = undefined;
|
||||
let osmObj: OsmObject = undefined
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
const n = new OsmNode(change.id)
|
||||
n.lat = change.changes["lat"]
|
||||
n.lon = change.changes["lon"]
|
||||
osmObj = n
|
||||
break;
|
||||
break
|
||||
case "way":
|
||||
const w = new OsmWay(change.id)
|
||||
w.nodes = change.changes["nodes"]
|
||||
osmObj = w
|
||||
break;
|
||||
break
|
||||
case "relation":
|
||||
const r = new OsmRelation(change.id)
|
||||
r.members = change.changes["members"]
|
||||
osmObj = r
|
||||
break;
|
||||
break
|
||||
}
|
||||
if (osmObj === undefined) {
|
||||
throw "Hmm? This is a bug"
|
||||
|
@ -442,55 +495,57 @@ export class Changes {
|
|||
let v = kv.v
|
||||
|
||||
if (v === "") {
|
||||
v = undefined;
|
||||
v = undefined
|
||||
}
|
||||
|
||||
const oldV = obj.tags[k]
|
||||
if (oldV === v) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
obj.tags[k] = v;
|
||||
changed = true;
|
||||
|
||||
|
||||
obj.tags[k] = v
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (change.changes !== undefined) {
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
// @ts-ignore
|
||||
const nlat = change.changes.lat;
|
||||
const nlat = change.changes.lat
|
||||
// @ts-ignore
|
||||
const nlon = change.changes.lon;
|
||||
const nlon = change.changes.lon
|
||||
const n = <OsmNode>obj
|
||||
if (n.lat !== nlat || n.lon !== nlon) {
|
||||
n.lat = nlat;
|
||||
n.lon = nlon;
|
||||
changed = true;
|
||||
n.lat = nlat
|
||||
n.lon = nlon
|
||||
changed = true
|
||||
}
|
||||
break;
|
||||
break
|
||||
case "way":
|
||||
const nnodes = change.changes["nodes"]
|
||||
const w = <OsmWay>obj
|
||||
if (!Utils.Identical(nnodes, w.nodes)) {
|
||||
w.nodes = nnodes
|
||||
changed = true;
|
||||
changed = true
|
||||
}
|
||||
break;
|
||||
break
|
||||
case "relation":
|
||||
const nmembers: { type: "node" | "way" | "relation", ref: number, role: string }[] = change.changes["members"]
|
||||
const nmembers: {
|
||||
type: "node" | "way" | "relation"
|
||||
ref: number
|
||||
role: string
|
||||
}[] = change.changes["members"]
|
||||
const r = <OsmRelation>obj
|
||||
if (!Utils.Identical(nmembers, r.members, (a, b) => {
|
||||
return a.role === b.role && a.type === b.type && a.ref === b.ref
|
||||
})) {
|
||||
r.members = nmembers;
|
||||
changed = true;
|
||||
if (
|
||||
!Utils.Identical(nmembers, r.members, (a, b) => {
|
||||
return a.role === b.role && a.type === b.type && a.ref === b.ref
|
||||
})
|
||||
) {
|
||||
r.members = nmembers
|
||||
changed = true
|
||||
}
|
||||
break;
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (changed && states.get(id) === "unchanged") {
|
||||
|
@ -498,15 +553,13 @@ export class Changes {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
const result = {
|
||||
newObjects: [],
|
||||
modifiedObjects: [],
|
||||
deletedObjects: []
|
||||
deletedObjects: [],
|
||||
}
|
||||
|
||||
objects.forEach((v, id) => {
|
||||
|
||||
const state = states.get(id)
|
||||
if (state === "created") {
|
||||
result.newObjects.push(v)
|
||||
|
@ -517,14 +570,21 @@ export class Changes {
|
|||
if (state === "deleted") {
|
||||
result.deletedObjects.push(v)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
console.debug("Calculated the pending changes: ", result.newObjects.length, "new; ", result.modifiedObjects.length, "modified;", result.deletedObjects, "deleted")
|
||||
console.debug(
|
||||
"Calculated the pending changes: ",
|
||||
result.newObjects.length,
|
||||
"new; ",
|
||||
result.modifiedObjects.length,
|
||||
"modified;",
|
||||
result.deletedObjects,
|
||||
"deleted"
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
public setHistoricalUserLocations(locations: FeatureSource ){
|
||||
|
||||
public setHistoricalUserLocations(locations: FeatureSource) {
|
||||
this.historicalUserLocations = locations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,26 @@
|
|||
import escapeHtml from "escape-html";
|
||||
import UserDetails, {OsmConnection} from "./OsmConnection";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import Locale from "../../UI/i18n/Locale";
|
||||
import Constants from "../../Models/Constants";
|
||||
import {Changes} from "./Changes";
|
||||
import {Utils} from "../../Utils";
|
||||
import escapeHtml from "escape-html"
|
||||
import UserDetails, { OsmConnection } from "./OsmConnection"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import { ElementStorage } from "../ElementStorage"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { Changes } from "./Changes"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export interface ChangesetTag {
|
||||
key: string,
|
||||
value: string | number,
|
||||
key: string
|
||||
value: string | number
|
||||
aggregate?: boolean
|
||||
}
|
||||
|
||||
export class ChangesetHandler {
|
||||
|
||||
private readonly allElements: ElementStorage;
|
||||
private osmConnection: OsmConnection;
|
||||
private readonly changes: Changes;
|
||||
private readonly _dryRun: UIEventSource<boolean>;
|
||||
private readonly userDetails: UIEventSource<UserDetails>;
|
||||
private readonly auth: any;
|
||||
private readonly backend: string;
|
||||
|
||||
private readonly allElements: ElementStorage
|
||||
private osmConnection: OsmConnection
|
||||
private readonly changes: Changes
|
||||
private readonly _dryRun: UIEventSource<boolean>
|
||||
private readonly userDetails: UIEventSource<UserDetails>
|
||||
private readonly auth: any
|
||||
private readonly backend: string
|
||||
|
||||
/**
|
||||
* Contains previously rewritten IDs
|
||||
|
@ -30,7 +28,6 @@ export class ChangesetHandler {
|
|||
*/
|
||||
private readonly _remappings = new Map<string, string>()
|
||||
|
||||
|
||||
/**
|
||||
* Use 'osmConnection.CreateChangesetHandler' instead
|
||||
* @param dryRun
|
||||
|
@ -39,36 +36,36 @@ export class ChangesetHandler {
|
|||
* @param changes
|
||||
* @param auth
|
||||
*/
|
||||
constructor(dryRun: UIEventSource<boolean>,
|
||||
osmConnection: OsmConnection,
|
||||
allElements: ElementStorage,
|
||||
changes: Changes,
|
||||
auth) {
|
||||
this.osmConnection = osmConnection;
|
||||
this.allElements = allElements;
|
||||
this.changes = changes;
|
||||
this._dryRun = dryRun;
|
||||
this.userDetails = osmConnection.userDetails;
|
||||
constructor(
|
||||
dryRun: UIEventSource<boolean>,
|
||||
osmConnection: OsmConnection,
|
||||
allElements: ElementStorage,
|
||||
changes: Changes,
|
||||
auth
|
||||
) {
|
||||
this.osmConnection = osmConnection
|
||||
this.allElements = allElements
|
||||
this.changes = changes
|
||||
this._dryRun = dryRun
|
||||
this.userDetails = osmConnection.userDetails
|
||||
this.backend = osmConnection._oauth_config.url
|
||||
this.auth = auth;
|
||||
this.auth = auth
|
||||
|
||||
if (dryRun) {
|
||||
console.log("DRYRUN ENABLED");
|
||||
console.log("DRYRUN ENABLED")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new list which contains every key at most once
|
||||
*
|
||||
*
|
||||
* ChangesetHandler.removeDuplicateMetaTags([{key: "k", value: "v"}, {key: "k0", value: "v0"}, {key: "k", value:"v"}] // => [{key: "k", value: "v"}, {key: "k0", value: "v0"}]
|
||||
*/
|
||||
public static removeDuplicateMetaTags(extraMetaTags: ChangesetTag[]): ChangesetTag[]{
|
||||
const r : ChangesetTag[] = []
|
||||
public static removeDuplicateMetaTags(extraMetaTags: ChangesetTag[]): ChangesetTag[] {
|
||||
const r: ChangesetTag[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const extraMetaTag of extraMetaTags) {
|
||||
if(seen.has(extraMetaTag.key)){
|
||||
if (seen.has(extraMetaTag.key)) {
|
||||
continue
|
||||
}
|
||||
r.push(extraMetaTag)
|
||||
|
@ -86,7 +83,7 @@ export class ChangesetHandler {
|
|||
* @private
|
||||
*/
|
||||
static rewriteMetaTags(extraMetaTags: ChangesetTag[], rewriteIds: Map<string, string>) {
|
||||
let hasChange = false;
|
||||
let hasChange = false
|
||||
for (const tag of extraMetaTags) {
|
||||
const match = tag.key.match(/^([a-zA-Z0-9_]+):(node\/-[0-9])$/)
|
||||
if (match == null) {
|
||||
|
@ -115,40 +112,48 @@ export class ChangesetHandler {
|
|||
public async UploadChangeset(
|
||||
generateChangeXML: (csid: number, remappings: Map<string, string>) => string,
|
||||
extraMetaTags: ChangesetTag[],
|
||||
openChangeset: UIEventSource<number>): Promise<void> {
|
||||
|
||||
if (!extraMetaTags.some(tag => tag.key === "comment") || !extraMetaTags.some(tag => tag.key === "theme")) {
|
||||
openChangeset: UIEventSource<number>
|
||||
): Promise<void> {
|
||||
if (
|
||||
!extraMetaTags.some((tag) => tag.key === "comment") ||
|
||||
!extraMetaTags.some((tag) => tag.key === "theme")
|
||||
) {
|
||||
throw "The meta tags should at least contain a `comment` and a `theme`"
|
||||
}
|
||||
|
||||
|
||||
extraMetaTags = [...extraMetaTags, ...this.defaultChangesetTags()]
|
||||
extraMetaTags = ChangesetHandler.removeDuplicateMetaTags(extraMetaTags)
|
||||
if (this.userDetails.data.csCount == 0) {
|
||||
// The user became a contributor!
|
||||
this.userDetails.data.csCount = 1;
|
||||
this.userDetails.ping();
|
||||
this.userDetails.data.csCount = 1
|
||||
this.userDetails.ping()
|
||||
}
|
||||
if (this._dryRun.data) {
|
||||
const changesetXML = generateChangeXML(123456, this._remappings);
|
||||
const changesetXML = generateChangeXML(123456, this._remappings)
|
||||
console.log("Metatags are", extraMetaTags)
|
||||
console.log(changesetXML);
|
||||
return;
|
||||
console.log(changesetXML)
|
||||
return
|
||||
}
|
||||
|
||||
if (openChangeset.data === undefined) {
|
||||
// We have to open a new changeset
|
||||
try {
|
||||
const csId = await this.OpenChangeset(extraMetaTags)
|
||||
openChangeset.setData(csId);
|
||||
const changeset = generateChangeXML(csId, this._remappings);
|
||||
console.trace("Opened a new changeset (openChangeset.data is undefined):", changeset);
|
||||
openChangeset.setData(csId)
|
||||
const changeset = generateChangeXML(csId, this._remappings)
|
||||
console.trace(
|
||||
"Opened a new changeset (openChangeset.data is undefined):",
|
||||
changeset
|
||||
)
|
||||
const changes = await this.UploadChange(csId, changeset)
|
||||
const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(extraMetaTags, changes)
|
||||
if(hasSpecialMotivationChanges){
|
||||
const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(
|
||||
extraMetaTags,
|
||||
changes
|
||||
)
|
||||
if (hasSpecialMotivationChanges) {
|
||||
// At this point, 'extraMetaTags' will have changed - we need to set the tags again
|
||||
this.UpdateTags(csId, extraMetaTags)
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Could not open/upload changeset due to ", e)
|
||||
openChangeset.setData(undefined)
|
||||
|
@ -156,29 +161,32 @@ export class ChangesetHandler {
|
|||
} else {
|
||||
// There still exists an open changeset (or at least we hope so)
|
||||
// Let's check!
|
||||
const csId = openChangeset.data;
|
||||
const csId = openChangeset.data
|
||||
try {
|
||||
|
||||
const oldChangesetMeta = await this.GetChangesetMeta(csId)
|
||||
if (!oldChangesetMeta.open) {
|
||||
// Mark the CS as closed...
|
||||
console.log("Could not fetch the metadata from the already open changeset")
|
||||
openChangeset.setData(undefined);
|
||||
// ... and try again. As the cs is closed, no recursive loop can exist
|
||||
openChangeset.setData(undefined)
|
||||
// ... and try again. As the cs is closed, no recursive loop can exist
|
||||
await this.UploadChangeset(generateChangeXML, extraMetaTags, openChangeset)
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const rewritings = await this.UploadChange(
|
||||
csId,
|
||||
generateChangeXML(csId, this._remappings))
|
||||
generateChangeXML(csId, this._remappings)
|
||||
)
|
||||
|
||||
const rewrittenTags = this.RewriteTagsOf(extraMetaTags, rewritings, oldChangesetMeta)
|
||||
const rewrittenTags = this.RewriteTagsOf(
|
||||
extraMetaTags,
|
||||
rewritings,
|
||||
oldChangesetMeta
|
||||
)
|
||||
await this.UpdateTags(csId, rewrittenTags)
|
||||
|
||||
} catch (e) {
|
||||
console.warn("Could not upload, changeset is probably closed: ", e);
|
||||
openChangeset.setData(undefined);
|
||||
console.warn("Could not upload, changeset is probably closed: ", e)
|
||||
openChangeset.setData(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -190,17 +198,17 @@ export class ChangesetHandler {
|
|||
* @param rewriteIds: the mapping of ids
|
||||
* @param oldChangesetMeta: the metadata-object of the already existing changeset
|
||||
*/
|
||||
public RewriteTagsOf(extraMetaTags: ChangesetTag[],
|
||||
rewriteIds: Map<string, string>,
|
||||
oldChangesetMeta: {
|
||||
open: boolean,
|
||||
id: number
|
||||
uid: number, // User ID
|
||||
changes_count: number,
|
||||
tags: any
|
||||
}) : ChangesetTag[] {
|
||||
|
||||
|
||||
public RewriteTagsOf(
|
||||
extraMetaTags: ChangesetTag[],
|
||||
rewriteIds: Map<string, string>,
|
||||
oldChangesetMeta: {
|
||||
open: boolean
|
||||
id: number
|
||||
uid: number // User ID
|
||||
changes_count: number
|
||||
tags: any
|
||||
}
|
||||
): ChangesetTag[] {
|
||||
// Note: extraMetaTags is where all the tags are collected into
|
||||
|
||||
// same as 'extraMetaTag', but indexed
|
||||
|
@ -221,7 +229,7 @@ export class ChangesetHandler {
|
|||
if (newMetaTag === undefined) {
|
||||
extraMetaTags.push({
|
||||
key: key,
|
||||
value: oldCsTags[key]
|
||||
value: oldCsTags[key],
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
@ -242,10 +250,8 @@ export class ChangesetHandler {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
ChangesetHandler.rewriteMetaTags(extraMetaTags, rewriteIds)
|
||||
return extraMetaTags
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -255,28 +261,28 @@ export class ChangesetHandler {
|
|||
* @private
|
||||
*/
|
||||
private static parseIdRewrite(node: any, type: string): [string, string] {
|
||||
const oldId = parseInt(node.attributes.old_id.value);
|
||||
const oldId = parseInt(node.attributes.old_id.value)
|
||||
if (node.attributes.new_id === undefined) {
|
||||
return [type+"/"+oldId, undefined];
|
||||
return [type + "/" + oldId, undefined]
|
||||
}
|
||||
|
||||
const newId = parseInt(node.attributes.new_id.value);
|
||||
const newId = parseInt(node.attributes.new_id.value)
|
||||
// The actual mapping
|
||||
const result: [string, string] = [type + "/" + oldId, type + "/" + newId]
|
||||
if(oldId === newId){
|
||||
return undefined;
|
||||
if (oldId === newId) {
|
||||
return undefined
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a diff-result XML of the form
|
||||
* Given a diff-result XML of the form
|
||||
* <diffResult version="0.6">
|
||||
* <node old_id="-1" new_id="9650458521" new_version="1"/>
|
||||
* <way old_id="-2" new_id="1050127772" new_version="1"/>
|
||||
* </diffResult>,
|
||||
* will:
|
||||
*
|
||||
*
|
||||
* - create a mapping `{'node/-1' --> "node/9650458521", 'way/-2' --> "way/9650458521"}
|
||||
* - Call this.changes.registerIdRewrites
|
||||
* - Call handleIdRewrites as needed
|
||||
|
@ -284,9 +290,9 @@ export class ChangesetHandler {
|
|||
* @private
|
||||
*/
|
||||
private parseUploadChangesetResponse(response: XMLDocument): Map<string, string> {
|
||||
const nodes = response.getElementsByTagName("node");
|
||||
const mappings : [string, string][]= []
|
||||
|
||||
const nodes = response.getElementsByTagName("node")
|
||||
const mappings: [string, string][] = []
|
||||
|
||||
for (const node of Array.from(nodes)) {
|
||||
const mapping = ChangesetHandler.parseIdRewrite(node, "node")
|
||||
if (mapping !== undefined) {
|
||||
|
@ -294,7 +300,7 @@ export class ChangesetHandler {
|
|||
}
|
||||
}
|
||||
|
||||
const ways = response.getElementsByTagName("way");
|
||||
const ways = response.getElementsByTagName("way")
|
||||
for (const way of Array.from(ways)) {
|
||||
const mapping = ChangesetHandler.parseIdRewrite(way, "way")
|
||||
if (mapping !== undefined) {
|
||||
|
@ -303,40 +309,41 @@ export class ChangesetHandler {
|
|||
}
|
||||
for (const mapping of mappings) {
|
||||
const [oldId, newId] = mapping
|
||||
this.allElements.addAlias(oldId, newId);
|
||||
if(newId !== undefined) {
|
||||
this.allElements.addAlias(oldId, newId)
|
||||
if (newId !== undefined) {
|
||||
this._remappings.set(mapping[0], mapping[1])
|
||||
}
|
||||
}
|
||||
return new Map<string, string>(mappings)
|
||||
|
||||
}
|
||||
|
||||
private async CloseChangeset(changesetId: number = undefined): Promise<void> {
|
||||
const self = this
|
||||
return new Promise<void>(function (resolve, reject) {
|
||||
if (changesetId === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
self.auth.xhr({
|
||||
method: 'PUT',
|
||||
path: '/api/0.6/changeset/' + changesetId + '/close',
|
||||
}, function (err, response) {
|
||||
if (response == null) {
|
||||
|
||||
console.log("err", err);
|
||||
self.auth.xhr(
|
||||
{
|
||||
method: "PUT",
|
||||
path: "/api/0.6/changeset/" + changesetId + "/close",
|
||||
},
|
||||
function (err, response) {
|
||||
if (response == null) {
|
||||
console.log("err", err)
|
||||
}
|
||||
console.log("Closed changeset ", changesetId)
|
||||
resolve()
|
||||
}
|
||||
console.log("Closed changeset ", changesetId)
|
||||
resolve()
|
||||
});
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async GetChangesetMeta(csId: number): Promise<{
|
||||
id: number,
|
||||
open: boolean,
|
||||
uid: number,
|
||||
changes_count: number,
|
||||
id: number
|
||||
open: boolean
|
||||
uid: number
|
||||
changes_count: number
|
||||
tags: any
|
||||
}> {
|
||||
const url = `${this.backend}/api/0.6/changeset/${csId}`
|
||||
|
@ -344,47 +351,59 @@ export class ChangesetHandler {
|
|||
return csData.elements[0]
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Puts the specified tags onto the changesets as they are.
|
||||
* This method will erase previously set tags
|
||||
*/
|
||||
private async UpdateTags(
|
||||
csId: number,
|
||||
tags: ChangesetTag[]) {
|
||||
private async UpdateTags(csId: number, tags: ChangesetTag[]) {
|
||||
tags = ChangesetHandler.removeDuplicateMetaTags(tags)
|
||||
|
||||
const self = this;
|
||||
const self = this
|
||||
return new Promise<string>(function (resolve, reject) {
|
||||
tags = Utils.NoNull(tags).filter(
|
||||
(tag) =>
|
||||
tag.key !== undefined &&
|
||||
tag.value !== undefined &&
|
||||
tag.key !== "" &&
|
||||
tag.value !== ""
|
||||
)
|
||||
const metadata = tags.map((kv) => `<tag k="${kv.key}" v="${escapeHtml(kv.value)}"/>`)
|
||||
|
||||
tags = Utils.NoNull(tags).filter(tag => tag.key !== undefined && tag.value !== undefined && tag.key !== "" && tag.value !== "")
|
||||
const metadata = tags.map(kv => `<tag k="${kv.key}" v="${escapeHtml(kv.value)}"/>`)
|
||||
|
||||
self.auth.xhr({
|
||||
method: 'PUT',
|
||||
path: '/api/0.6/changeset/' + csId,
|
||||
options: {header: {'Content-Type': 'text/xml'}},
|
||||
content: [`<osm><changeset>`,
|
||||
metadata,
|
||||
`</changeset></osm>`].join("")
|
||||
}, function (err, response) {
|
||||
if (response === undefined) {
|
||||
console.error("Updating the tags of changeset "+csId+" failed:", err);
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(response);
|
||||
self.auth.xhr(
|
||||
{
|
||||
method: "PUT",
|
||||
path: "/api/0.6/changeset/" + csId,
|
||||
options: { header: { "Content-Type": "text/xml" } },
|
||||
content: [`<osm><changeset>`, metadata, `</changeset></osm>`].join(""),
|
||||
},
|
||||
function (err, response) {
|
||||
if (response === undefined) {
|
||||
console.error("Updating the tags of changeset " + csId + " failed:", err)
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(response)
|
||||
}
|
||||
}
|
||||
});
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private defaultChangesetTags() : ChangesetTag[]{
|
||||
return [ ["created_by", `MapComplete ${Constants.vNumber}`],
|
||||
|
||||
private defaultChangesetTags(): ChangesetTag[] {
|
||||
return [
|
||||
["created_by", `MapComplete ${Constants.vNumber}`],
|
||||
["locale", Locale.language.data],
|
||||
["host", `${window.location.origin}${window.location.pathname}`],
|
||||
["source", this.changes.state["currentUserLocation"]?.features?.data?.length > 0 ? "survey" : undefined],
|
||||
["imagery", this.changes.state["backgroundLayer"]?.data?.id]].map(([key, value]) => ({
|
||||
key, value, aggretage: false
|
||||
[
|
||||
"source",
|
||||
this.changes.state["currentUserLocation"]?.features?.data?.length > 0
|
||||
? "survey"
|
||||
: undefined,
|
||||
],
|
||||
["imagery", this.changes.state["backgroundLayer"]?.data?.id],
|
||||
].map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
aggretage: false,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -394,61 +413,57 @@ export class ChangesetHandler {
|
|||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
private OpenChangeset(
|
||||
changesetTags: ChangesetTag[]
|
||||
): Promise<number> {
|
||||
const self = this;
|
||||
private OpenChangeset(changesetTags: ChangesetTag[]): Promise<number> {
|
||||
const self = this
|
||||
return new Promise<number>(function (resolve, reject) {
|
||||
|
||||
const metadata = changesetTags.map(cstag => [cstag.key, cstag.value])
|
||||
.filter(kv => (kv[1] ?? "") !== "")
|
||||
.map(kv => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`)
|
||||
const metadata = changesetTags
|
||||
.map((cstag) => [cstag.key, cstag.value])
|
||||
.filter((kv) => (kv[1] ?? "") !== "")
|
||||
.map((kv) => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`)
|
||||
.join("\n")
|
||||
|
||||
|
||||
self.auth.xhr({
|
||||
method: 'PUT',
|
||||
path: '/api/0.6/changeset/create',
|
||||
options: {header: {'Content-Type': 'text/xml'}},
|
||||
content: [`<osm><changeset>`,
|
||||
metadata,
|
||||
`</changeset></osm>`].join("")
|
||||
}, function (err, response) {
|
||||
if (response === undefined) {
|
||||
console.error("Opening a changeset failed:", err);
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(Number(response));
|
||||
self.auth.xhr(
|
||||
{
|
||||
method: "PUT",
|
||||
path: "/api/0.6/changeset/create",
|
||||
options: { header: { "Content-Type": "text/xml" } },
|
||||
content: [`<osm><changeset>`, metadata, `</changeset></osm>`].join(""),
|
||||
},
|
||||
function (err, response) {
|
||||
if (response === undefined) {
|
||||
console.error("Opening a changeset failed:", err)
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(Number(response))
|
||||
}
|
||||
}
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a changesetXML
|
||||
*/
|
||||
private UploadChange(changesetId: number,
|
||||
changesetXML: string): Promise<Map<string, string>> {
|
||||
const self = this;
|
||||
private UploadChange(changesetId: number, changesetXML: string): Promise<Map<string, string>> {
|
||||
const self = this
|
||||
return new Promise(function (resolve, reject) {
|
||||
self.auth.xhr({
|
||||
method: 'POST',
|
||||
options: {header: {'Content-Type': 'text/xml'}},
|
||||
path: '/api/0.6/changeset/' + changesetId + '/upload',
|
||||
content: changesetXML
|
||||
}, function (err, response) {
|
||||
if (response == null) {
|
||||
console.error("Uploading an actual change failed", err);
|
||||
reject(err);
|
||||
self.auth.xhr(
|
||||
{
|
||||
method: "POST",
|
||||
options: { header: { "Content-Type": "text/xml" } },
|
||||
path: "/api/0.6/changeset/" + changesetId + "/upload",
|
||||
content: changesetXML,
|
||||
},
|
||||
function (err, response) {
|
||||
if (response == null) {
|
||||
console.error("Uploading an actual change failed", err)
|
||||
reject(err)
|
||||
}
|
||||
const changes = self.parseUploadChangesetResponse(response)
|
||||
console.log("Uploaded changeset ", changesetId)
|
||||
resolve(changes)
|
||||
}
|
||||
const changes = self.parseUploadChangesetResponse(response);
|
||||
console.log("Uploaded changeset ", changesetId);
|
||||
resolve(changes);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
import State from "../../State";
|
||||
import {Utils} from "../../Utils";
|
||||
import {BBox} from "../BBox";
|
||||
import State from "../../State"
|
||||
import { Utils } from "../../Utils"
|
||||
import { BBox } from "../BBox"
|
||||
|
||||
export interface GeoCodeResult {
|
||||
display_name: string,
|
||||
lat: number, lon: number, boundingbox: number[],
|
||||
osm_type: "node" | "way" | "relation",
|
||||
display_name: string
|
||||
lat: number
|
||||
lon: number
|
||||
boundingbox: number[]
|
||||
osm_type: "node" | "way" | "relation"
|
||||
osm_id: string
|
||||
}
|
||||
|
||||
export class Geocoding {
|
||||
|
||||
private static readonly host = "https://nominatim.openstreetmap.org/search?";
|
||||
private static readonly host = "https://nominatim.openstreetmap.org/search?"
|
||||
|
||||
static async Search(query: string): Promise<GeoCodeResult[]> {
|
||||
const b = State?.state?.currentBounds?.data ?? BBox.global;
|
||||
const url = Geocoding.host + "format=json&limit=1&viewbox=" +
|
||||
const b = State?.state?.currentBounds?.data ?? BBox.global
|
||||
const url =
|
||||
Geocoding.host +
|
||||
"format=json&limit=1&viewbox=" +
|
||||
`${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` +
|
||||
"&accept-language=nl&q=" + query;
|
||||
return Utils.downloadJson(url)
|
||||
"&accept-language=nl&q=" +
|
||||
query
|
||||
return Utils.downloadJson(url)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,153 +1,161 @@
|
|||
import osmAuth from "osm-auth";
|
||||
import {Store, Stores, UIEventSource} from "../UIEventSource";
|
||||
import {OsmPreferences} from "./OsmPreferences";
|
||||
import {ChangesetHandler} from "./ChangesetHandler";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import Svg from "../../Svg";
|
||||
import Img from "../../UI/Base/Img";
|
||||
import {Utils} from "../../Utils";
|
||||
import {OsmObject} from "./OsmObject";
|
||||
import {Changes} from "./Changes";
|
||||
import osmAuth from "osm-auth"
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import { OsmPreferences } from "./OsmPreferences"
|
||||
import { ChangesetHandler } from "./ChangesetHandler"
|
||||
import { ElementStorage } from "../ElementStorage"
|
||||
import Svg from "../../Svg"
|
||||
import Img from "../../UI/Base/Img"
|
||||
import { Utils } from "../../Utils"
|
||||
import { OsmObject } from "./OsmObject"
|
||||
import { Changes } from "./Changes"
|
||||
|
||||
export default class UserDetails {
|
||||
|
||||
public loggedIn = false;
|
||||
public name = "Not logged in";
|
||||
public uid: number;
|
||||
public csCount = 0;
|
||||
public img: string;
|
||||
public unreadMessages = 0;
|
||||
public totalMessages = 0;
|
||||
home: { lon: number; lat: number };
|
||||
public backend: string;
|
||||
public loggedIn = false
|
||||
public name = "Not logged in"
|
||||
public uid: number
|
||||
public csCount = 0
|
||||
public img: string
|
||||
public unreadMessages = 0
|
||||
public totalMessages = 0
|
||||
home: { lon: number; lat: number }
|
||||
public backend: string
|
||||
|
||||
constructor(backend: string) {
|
||||
this.backend = backend;
|
||||
this.backend = backend
|
||||
}
|
||||
}
|
||||
|
||||
export class OsmConnection {
|
||||
|
||||
public static readonly oauth_configs = {
|
||||
"osm": {
|
||||
oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem',
|
||||
oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI',
|
||||
url: "https://www.openstreetmap.org"
|
||||
osm: {
|
||||
oauth_consumer_key: "hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem",
|
||||
oauth_secret: "wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI",
|
||||
url: "https://www.openstreetmap.org",
|
||||
},
|
||||
"osm-test": {
|
||||
oauth_consumer_key: 'Zgr7EoKb93uwPv2EOFkIlf3n9NLwj5wbyfjZMhz2',
|
||||
oauth_secret: '3am1i1sykHDMZ66SGq4wI2Z7cJMKgzneCHp3nctn',
|
||||
url: "https://master.apis.dev.openstreetmap.org"
|
||||
}
|
||||
|
||||
|
||||
oauth_consumer_key: "Zgr7EoKb93uwPv2EOFkIlf3n9NLwj5wbyfjZMhz2",
|
||||
oauth_secret: "3am1i1sykHDMZ66SGq4wI2Z7cJMKgzneCHp3nctn",
|
||||
url: "https://master.apis.dev.openstreetmap.org",
|
||||
},
|
||||
}
|
||||
public auth;
|
||||
public userDetails: UIEventSource<UserDetails>;
|
||||
public auth
|
||||
public userDetails: UIEventSource<UserDetails>
|
||||
public isLoggedIn: Store<boolean>
|
||||
public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">("not-attempted")
|
||||
public preferencesHandler: OsmPreferences;
|
||||
public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">(
|
||||
"not-attempted"
|
||||
)
|
||||
public preferencesHandler: OsmPreferences
|
||||
public readonly _oauth_config: {
|
||||
oauth_consumer_key: string,
|
||||
oauth_secret: string,
|
||||
oauth_consumer_key: string
|
||||
oauth_secret: string
|
||||
url: string
|
||||
};
|
||||
private readonly _dryRun: UIEventSource<boolean>;
|
||||
private fakeUser: boolean;
|
||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
|
||||
private readonly _iframeMode: Boolean | boolean;
|
||||
private readonly _singlePage: boolean;
|
||||
private isChecking = false;
|
||||
}
|
||||
private readonly _dryRun: UIEventSource<boolean>
|
||||
private fakeUser: boolean
|
||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []
|
||||
private readonly _iframeMode: Boolean | boolean
|
||||
private readonly _singlePage: boolean
|
||||
private isChecking = false
|
||||
|
||||
constructor(options: {
|
||||
dryRun?: UIEventSource<boolean>,
|
||||
fakeUser?: false | boolean,
|
||||
oauth_token?: UIEventSource<string>,
|
||||
// Used to keep multiple changesets open and to write to the correct changeset
|
||||
singlePage?: boolean,
|
||||
osmConfiguration?: "osm" | "osm-test",
|
||||
attemptLogin?: true | boolean
|
||||
}
|
||||
) {
|
||||
this.fakeUser = options.fakeUser ?? false;
|
||||
this._singlePage = options.singlePage ?? true;
|
||||
this._oauth_config = OsmConnection.oauth_configs[options.osmConfiguration ?? 'osm'] ?? OsmConnection.oauth_configs.osm;
|
||||
dryRun?: UIEventSource<boolean>
|
||||
fakeUser?: false | boolean
|
||||
oauth_token?: UIEventSource<string>
|
||||
// Used to keep multiple changesets open and to write to the correct changeset
|
||||
singlePage?: boolean
|
||||
osmConfiguration?: "osm" | "osm-test"
|
||||
attemptLogin?: true | boolean
|
||||
}) {
|
||||
this.fakeUser = options.fakeUser ?? false
|
||||
this._singlePage = options.singlePage ?? true
|
||||
this._oauth_config =
|
||||
OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ??
|
||||
OsmConnection.oauth_configs.osm
|
||||
console.debug("Using backend", this._oauth_config.url)
|
||||
OsmObject.SetBackendUrl(this._oauth_config.url + "/")
|
||||
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
|
||||
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top
|
||||
|
||||
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails");
|
||||
this.userDetails = new UIEventSource<UserDetails>(
|
||||
new UserDetails(this._oauth_config.url),
|
||||
"userDetails"
|
||||
)
|
||||
if (options.fakeUser) {
|
||||
const ud = this.userDetails.data;
|
||||
const ud = this.userDetails.data
|
||||
ud.csCount = 5678
|
||||
ud.loggedIn = true;
|
||||
ud.loggedIn = true
|
||||
ud.unreadMessages = 0
|
||||
ud.name = "Fake user"
|
||||
ud.totalMessages = 42;
|
||||
ud.totalMessages = 42
|
||||
}
|
||||
const self = this;
|
||||
this.isLoggedIn = this.userDetails.map(user => user.loggedIn);
|
||||
this.isLoggedIn.addCallback(isLoggedIn => {
|
||||
const self = this
|
||||
this.isLoggedIn = this.userDetails.map((user) => user.loggedIn)
|
||||
this.isLoggedIn.addCallback((isLoggedIn) => {
|
||||
if (self.userDetails.data.loggedIn == false && isLoggedIn == true) {
|
||||
// We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do
|
||||
// This means someone attempted to toggle this; so we attempt to login!
|
||||
self.AttemptLogin()
|
||||
}
|
||||
});
|
||||
|
||||
this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false);
|
||||
})
|
||||
|
||||
this.updateAuthObject();
|
||||
this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false)
|
||||
|
||||
this.preferencesHandler = new OsmPreferences(this.auth, this);
|
||||
this.updateAuthObject()
|
||||
|
||||
this.preferencesHandler = new OsmPreferences(this.auth, this)
|
||||
|
||||
if (options.oauth_token?.data !== undefined) {
|
||||
console.log(options.oauth_token.data)
|
||||
const self = this;
|
||||
this.auth.bootstrapToken(options.oauth_token.data,
|
||||
const self = this
|
||||
this.auth.bootstrapToken(
|
||||
options.oauth_token.data,
|
||||
(x) => {
|
||||
console.log("Called back: ", x)
|
||||
self.AttemptLogin();
|
||||
}, this.auth);
|
||||
|
||||
options.oauth_token.setData(undefined);
|
||||
self.AttemptLogin()
|
||||
},
|
||||
this.auth
|
||||
)
|
||||
|
||||
options.oauth_token.setData(undefined)
|
||||
}
|
||||
if (this.auth.authenticated() && (options.attemptLogin !== false)) {
|
||||
this.AttemptLogin(); // Also updates the user badge
|
||||
if (this.auth.authenticated() && options.attemptLogin !== false) {
|
||||
this.AttemptLogin() // Also updates the user badge
|
||||
} else {
|
||||
console.log("Not authenticated");
|
||||
console.log("Not authenticated")
|
||||
}
|
||||
}
|
||||
|
||||
public CreateChangesetHandler(allElements: ElementStorage, changes: Changes){
|
||||
return new ChangesetHandler(this._dryRun, this, allElements, changes, this.auth);
|
||||
|
||||
public CreateChangesetHandler(allElements: ElementStorage, changes: Changes) {
|
||||
return new ChangesetHandler(this._dryRun, this, allElements, changes, this.auth)
|
||||
}
|
||||
|
||||
public GetPreference(key: string, defaultValue: string = undefined, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||
return this.preferencesHandler.GetPreference(key, defaultValue, prefix);
|
||||
public GetPreference(
|
||||
key: string,
|
||||
defaultValue: string = undefined,
|
||||
prefix: string = "mapcomplete-"
|
||||
): UIEventSource<string> {
|
||||
return this.preferencesHandler.GetPreference(key, defaultValue, prefix)
|
||||
}
|
||||
|
||||
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||
return this.preferencesHandler.GetLongPreference(key, prefix);
|
||||
return this.preferencesHandler.GetLongPreference(key, prefix)
|
||||
}
|
||||
|
||||
public OnLoggedIn(action: (userDetails: UserDetails) => void) {
|
||||
this._onLoggedIn.push(action);
|
||||
this._onLoggedIn.push(action)
|
||||
}
|
||||
|
||||
public LogOut() {
|
||||
this.auth.logout();
|
||||
this.userDetails.data.loggedIn = false;
|
||||
this.userDetails.data.csCount = 0;
|
||||
this.userDetails.data.name = "";
|
||||
this.userDetails.ping();
|
||||
this.auth.logout()
|
||||
this.userDetails.data.loggedIn = false
|
||||
this.userDetails.data.csCount = 0
|
||||
this.userDetails.data.name = ""
|
||||
this.userDetails.ping()
|
||||
console.log("Logged out")
|
||||
this.loadingStatus.setData("not-attempted")
|
||||
}
|
||||
|
||||
|
||||
public Backend(): string {
|
||||
return this._oauth_config.url;
|
||||
return this._oauth_config.url
|
||||
}
|
||||
|
||||
public AttemptLogin() {
|
||||
|
@ -155,76 +163,81 @@ export class OsmConnection {
|
|||
if (this.fakeUser) {
|
||||
this.loadingStatus.setData("logged-in")
|
||||
console.log("AttemptLogin called, but ignored as fakeUser is set")
|
||||
return;
|
||||
return
|
||||
}
|
||||
const self = this;
|
||||
console.log("Trying to log in...");
|
||||
this.updateAuthObject();
|
||||
this.auth.xhr({
|
||||
method: 'GET',
|
||||
path: '/api/0.6/user/details'
|
||||
}, function (err, details) {
|
||||
if (err != null) {
|
||||
console.log(err);
|
||||
self.loadingStatus.setData("error")
|
||||
if (err.status == 401) {
|
||||
console.log("Clearing tokens...")
|
||||
// Not authorized - our token probably got revoked
|
||||
// Reset all the tokens
|
||||
const tokens = [
|
||||
"https://www.openstreetmap.orgoauth_request_token_secret",
|
||||
"https://www.openstreetmap.orgoauth_token",
|
||||
"https://www.openstreetmap.orgoauth_token_secret"]
|
||||
tokens.forEach(token => localStorage.removeItem(token))
|
||||
const self = this
|
||||
console.log("Trying to log in...")
|
||||
this.updateAuthObject()
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "GET",
|
||||
path: "/api/0.6/user/details",
|
||||
},
|
||||
function (err, details) {
|
||||
if (err != null) {
|
||||
console.log(err)
|
||||
self.loadingStatus.setData("error")
|
||||
if (err.status == 401) {
|
||||
console.log("Clearing tokens...")
|
||||
// Not authorized - our token probably got revoked
|
||||
// Reset all the tokens
|
||||
const tokens = [
|
||||
"https://www.openstreetmap.orgoauth_request_token_secret",
|
||||
"https://www.openstreetmap.orgoauth_token",
|
||||
"https://www.openstreetmap.orgoauth_token_secret",
|
||||
]
|
||||
tokens.forEach((token) => localStorage.removeItem(token))
|
||||
}
|
||||
return
|
||||
}
|
||||
return;
|
||||
|
||||
if (details == null) {
|
||||
self.loadingStatus.setData("error")
|
||||
return
|
||||
}
|
||||
|
||||
self.CheckForMessagesContinuously()
|
||||
|
||||
// details is an XML DOM of user details
|
||||
let userInfo = details.getElementsByTagName("user")[0]
|
||||
|
||||
// let moreDetails = new DOMParser().parseFromString(userInfo.innerHTML, "text/xml");
|
||||
|
||||
let data = self.userDetails.data
|
||||
data.loggedIn = true
|
||||
console.log("Login completed, userinfo is ", userInfo)
|
||||
data.name = userInfo.getAttribute("display_name")
|
||||
data.uid = Number(userInfo.getAttribute("id"))
|
||||
data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count")
|
||||
|
||||
data.img = undefined
|
||||
const imgEl = userInfo.getElementsByTagName("img")
|
||||
if (imgEl !== undefined && imgEl[0] !== undefined) {
|
||||
data.img = imgEl[0].getAttribute("href")
|
||||
}
|
||||
data.img = data.img ?? Img.AsData(Svg.osm_logo)
|
||||
|
||||
const homeEl = userInfo.getElementsByTagName("home")
|
||||
if (homeEl !== undefined && homeEl[0] !== undefined) {
|
||||
const lat = parseFloat(homeEl[0].getAttribute("lat"))
|
||||
const lon = parseFloat(homeEl[0].getAttribute("lon"))
|
||||
data.home = { lat: lat, lon: lon }
|
||||
}
|
||||
|
||||
self.loadingStatus.setData("logged-in")
|
||||
const messages = userInfo
|
||||
.getElementsByTagName("messages")[0]
|
||||
.getElementsByTagName("received")[0]
|
||||
data.unreadMessages = parseInt(messages.getAttribute("unread"))
|
||||
data.totalMessages = parseInt(messages.getAttribute("count"))
|
||||
|
||||
self.userDetails.ping()
|
||||
for (const action of self._onLoggedIn) {
|
||||
action(self.userDetails.data)
|
||||
}
|
||||
self._onLoggedIn = []
|
||||
}
|
||||
|
||||
if (details == null) {
|
||||
self.loadingStatus.setData("error")
|
||||
return;
|
||||
}
|
||||
|
||||
self.CheckForMessagesContinuously();
|
||||
|
||||
// details is an XML DOM of user details
|
||||
let userInfo = details.getElementsByTagName("user")[0];
|
||||
|
||||
// let moreDetails = new DOMParser().parseFromString(userInfo.innerHTML, "text/xml");
|
||||
|
||||
let data = self.userDetails.data;
|
||||
data.loggedIn = true;
|
||||
console.log("Login completed, userinfo is ", userInfo);
|
||||
data.name = userInfo.getAttribute('display_name');
|
||||
data.uid = Number(userInfo.getAttribute("id"))
|
||||
data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count");
|
||||
|
||||
data.img = undefined;
|
||||
const imgEl = userInfo.getElementsByTagName("img");
|
||||
if (imgEl !== undefined && imgEl[0] !== undefined) {
|
||||
data.img = imgEl[0].getAttribute("href");
|
||||
}
|
||||
data.img = data.img ?? Img.AsData(Svg.osm_logo);
|
||||
|
||||
const homeEl = userInfo.getElementsByTagName("home");
|
||||
if (homeEl !== undefined && homeEl[0] !== undefined) {
|
||||
const lat = parseFloat(homeEl[0].getAttribute("lat"));
|
||||
const lon = parseFloat(homeEl[0].getAttribute("lon"));
|
||||
data.home = {lat: lat, lon: lon};
|
||||
}
|
||||
|
||||
self.loadingStatus.setData("logged-in")
|
||||
const messages = userInfo.getElementsByTagName("messages")[0].getElementsByTagName("received")[0];
|
||||
data.unreadMessages = parseInt(messages.getAttribute("unread"));
|
||||
data.totalMessages = parseInt(messages.getAttribute("count"));
|
||||
|
||||
self.userDetails.ping();
|
||||
for (const action of self._onLoggedIn) {
|
||||
action(self.userDetails.data);
|
||||
}
|
||||
self._onLoggedIn = [];
|
||||
|
||||
});
|
||||
)
|
||||
}
|
||||
|
||||
public closeNote(id: number | string, text?: string): Promise<void> {
|
||||
|
@ -236,22 +249,23 @@ export class OsmConnection {
|
|||
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text)
|
||||
return new Promise((ok) => {
|
||||
ok()
|
||||
});
|
||||
})
|
||||
}
|
||||
return new Promise((ok, error) => {
|
||||
this.auth.xhr({
|
||||
method: 'POST',
|
||||
path: `/api/0.6/notes/${id}/close${textSuffix}`,
|
||||
}, function (err, _) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
ok()
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/0.6/notes/${id}/close${textSuffix}`,
|
||||
},
|
||||
function (err, _) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
ok()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public reopenNote(id: number | string, text?: string): Promise<void> {
|
||||
|
@ -259,110 +273,118 @@ export class OsmConnection {
|
|||
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text)
|
||||
return new Promise((ok) => {
|
||||
ok()
|
||||
});
|
||||
})
|
||||
}
|
||||
let textSuffix = ""
|
||||
if ((text ?? "") !== "") {
|
||||
textSuffix = "?text=" + encodeURIComponent(text)
|
||||
}
|
||||
return new Promise((ok, error) => {
|
||||
this.auth.xhr({
|
||||
method: 'POST',
|
||||
path: `/api/0.6/notes/${id}/reopen${textSuffix}`
|
||||
}, function (err, _) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
ok()
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/0.6/notes/${id}/reopen${textSuffix}`,
|
||||
},
|
||||
function (err, _) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
ok()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> {
|
||||
if (this._dryRun.data) {
|
||||
console.warn("Dryrun enabled - not actually opening note with text ", text)
|
||||
return new Promise<{ id: number }>((ok) => {
|
||||
window.setTimeout(() => ok({id: Math.floor(Math.random() * 1000)}), Math.random() * 5000)
|
||||
});
|
||||
}
|
||||
const auth = this.auth;
|
||||
const content = {lat, lon, text}
|
||||
return new Promise((ok, error) => {
|
||||
auth.xhr({
|
||||
method: 'POST',
|
||||
path: `/api/0.6/notes.json`,
|
||||
options: {
|
||||
header:
|
||||
{'Content-Type': 'application/json'}
|
||||
},
|
||||
content: JSON.stringify(content)
|
||||
|
||||
}, function (
|
||||
err,
|
||||
response: string) {
|
||||
console.log("RESPONSE IS", response)
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
const parsed = JSON.parse(response)
|
||||
const id = parsed.properties.id
|
||||
console.log("OPENED NOTE", id)
|
||||
ok({id})
|
||||
}
|
||||
window.setTimeout(
|
||||
() => ok({ id: Math.floor(Math.random() * 1000) }),
|
||||
Math.random() * 5000
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
const auth = this.auth
|
||||
const content = { lat, lon, text }
|
||||
return new Promise((ok, error) => {
|
||||
auth.xhr(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/0.6/notes.json`,
|
||||
options: {
|
||||
header: { "Content-Type": "application/json" },
|
||||
},
|
||||
content: JSON.stringify(content),
|
||||
},
|
||||
function (err, response: string) {
|
||||
console.log("RESPONSE IS", response)
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
const parsed = JSON.parse(response)
|
||||
const id = parsed.properties.id
|
||||
console.log("OPENED NOTE", id)
|
||||
ok({ id })
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
public addCommentToNote(id: number | string, text: string): Promise<void> {
|
||||
if (this._dryRun.data) {
|
||||
console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id)
|
||||
return new Promise((ok) => {
|
||||
ok()
|
||||
});
|
||||
})
|
||||
}
|
||||
if ((text ?? "") === "") {
|
||||
throw "Invalid text!"
|
||||
}
|
||||
|
||||
return new Promise((ok, error) => {
|
||||
this.auth.xhr({
|
||||
method: 'POST',
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "POST",
|
||||
|
||||
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`
|
||||
}, function (err, _) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
ok()
|
||||
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`,
|
||||
},
|
||||
function (err, _) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
ok()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private updateAuthObject() {
|
||||
let pwaStandAloneMode = false;
|
||||
let pwaStandAloneMode = false
|
||||
try {
|
||||
if (Utils.runningFromConsole) {
|
||||
pwaStandAloneMode = true
|
||||
} else if (window.matchMedia('(display-mode: standalone)').matches || window.matchMedia('(display-mode: fullscreen)').matches) {
|
||||
pwaStandAloneMode = true;
|
||||
} else if (
|
||||
window.matchMedia("(display-mode: standalone)").matches ||
|
||||
window.matchMedia("(display-mode: fullscreen)").matches
|
||||
) {
|
||||
pwaStandAloneMode = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Detecting standalone mode failed", e, ". Assuming in browser and not worrying furhter")
|
||||
console.warn(
|
||||
"Detecting standalone mode failed",
|
||||
e,
|
||||
". Assuming in browser and not worrying furhter"
|
||||
)
|
||||
}
|
||||
const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage;
|
||||
const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage
|
||||
|
||||
// In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
|
||||
// Same for an iframe...
|
||||
|
||||
|
||||
this.auth = new osmAuth({
|
||||
oauth_consumer_key: this._oauth_config.oauth_consumer_key,
|
||||
oauth_secret: this._oauth_config.oauth_secret,
|
||||
|
@ -370,22 +392,20 @@ export class OsmConnection {
|
|||
landing: standalone ? undefined : window.location.href,
|
||||
singlepage: !standalone,
|
||||
auto: true,
|
||||
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
private CheckForMessagesContinuously() {
|
||||
const self = this;
|
||||
const self = this
|
||||
if (this.isChecking) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this.isChecking = true;
|
||||
Stores.Chronic(5 * 60 * 1000).addCallback(_ => {
|
||||
this.isChecking = true
|
||||
Stores.Chronic(5 * 60 * 1000).addCallback((_) => {
|
||||
if (self.isLoggedIn.data) {
|
||||
console.log("Checking for messages")
|
||||
self.AttemptLogin();
|
||||
self.AttemptLogin()
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
import {Utils} from "../../Utils";
|
||||
import * as polygon_features from "../../assets/polygon-features.json";
|
||||
import {Store, UIEventSource} from "../UIEventSource";
|
||||
import {BBox} from "../BBox";
|
||||
import * as OsmToGeoJson from "osmtogeojson";
|
||||
import { Utils } from "../../Utils"
|
||||
import * as polygon_features from "../../assets/polygon-features.json"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { BBox } from "../BBox"
|
||||
import * as OsmToGeoJson from "osmtogeojson"
|
||||
import { NodeId, OsmFeature, OsmId, OsmTags, RelationId, WayId } from "../../Models/OsmFeature"
|
||||
|
||||
export abstract class OsmObject {
|
||||
|
||||
private static defaultBackend = "https://www.openstreetmap.org/"
|
||||
protected static backendURL = OsmObject.defaultBackend;
|
||||
protected static backendURL = OsmObject.defaultBackend
|
||||
private static polygonFeatures = OsmObject.constructPolygonFeatures()
|
||||
private static objectCache = new Map<string, UIEventSource<OsmObject>>();
|
||||
private static historyCache = new Map<string, UIEventSource<OsmObject[]>>();
|
||||
type: "node" | "way" | "relation";
|
||||
id: number;
|
||||
private static objectCache = new Map<string, UIEventSource<OsmObject>>()
|
||||
private static historyCache = new Map<string, UIEventSource<OsmObject[]>>()
|
||||
type: "node" | "way" | "relation"
|
||||
id: number
|
||||
/**
|
||||
* The OSM tags as simple object
|
||||
*/
|
||||
tags: {} = {};
|
||||
version: number;
|
||||
public changed: boolean = false;
|
||||
timestamp: Date;
|
||||
tags: OsmTags
|
||||
version: number
|
||||
public changed: boolean = false
|
||||
timestamp: Date
|
||||
|
||||
protected constructor(type: string, id: number) {
|
||||
this.id = id;
|
||||
this.id = id
|
||||
// @ts-ignore
|
||||
this.type = type;
|
||||
this.type = type
|
||||
this.tags = {
|
||||
id: `${this.type}/${id}`
|
||||
id: `${this.type}/${id}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,58 +37,63 @@ export abstract class OsmObject {
|
|||
if (!url.startsWith("http")) {
|
||||
throw "Backend URL must begin with http"
|
||||
}
|
||||
this.backendURL = url;
|
||||
this.backendURL = url
|
||||
}
|
||||
|
||||
public static DownloadObject(id: string, forceRefresh: boolean = false): Store<OsmObject> {
|
||||
let src: UIEventSource<OsmObject>;
|
||||
let src: UIEventSource<OsmObject>
|
||||
if (OsmObject.objectCache.has(id)) {
|
||||
src = OsmObject.objectCache.get(id)
|
||||
if (forceRefresh) {
|
||||
src.setData(undefined)
|
||||
} else {
|
||||
return src;
|
||||
return src
|
||||
}
|
||||
} else {
|
||||
src = UIEventSource.FromPromise(OsmObject.DownloadObjectAsync(id))
|
||||
}
|
||||
|
||||
OsmObject.objectCache.set(id, src);
|
||||
return src;
|
||||
OsmObject.objectCache.set(id, src)
|
||||
return src
|
||||
}
|
||||
|
||||
static async DownloadPropertiesOf(id: string): Promise<any> {
|
||||
const splitted = id.split("/");
|
||||
const idN = Number(splitted[1]);
|
||||
const splitted = id.split("/")
|
||||
const idN = Number(splitted[1])
|
||||
if (idN < 0) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
const url = `${OsmObject.backendURL}api/0.6/${id}`;
|
||||
const url = `${OsmObject.backendURL}api/0.6/${id}`
|
||||
const rawData = await Utils.downloadJsonCached(url, 1000)
|
||||
return rawData.elements[0].tags
|
||||
}
|
||||
|
||||
static async DownloadObjectAsync(id: NodeId): Promise<OsmNode | undefined>
|
||||
static async DownloadObjectAsync(id: WayId): Promise<OsmWay | undefined>
|
||||
static async DownloadObjectAsync(id: RelationId): Promise<OsmRelation | undefined>
|
||||
static async DownloadObjectAsync(id: OsmId): Promise<OsmObject | undefined>
|
||||
static async DownloadObjectAsync(id: string): Promise<OsmObject | undefined>
|
||||
static async DownloadObjectAsync(id: string): Promise<OsmObject | undefined> {
|
||||
const splitted = id.split("/");
|
||||
const type = splitted[0];
|
||||
const idN = Number(splitted[1]);
|
||||
const splitted = id.split("/")
|
||||
const type = splitted[0]
|
||||
const idN = Number(splitted[1])
|
||||
if (idN < 0) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
const full = (!id.startsWith("node")) ? "/full" : "";
|
||||
const url = `${OsmObject.backendURL}api/0.6/${id}${full}`;
|
||||
const full = !id.startsWith("node") ? "/full" : ""
|
||||
const url = `${OsmObject.backendURL}api/0.6/${id}${full}`
|
||||
const rawData = await Utils.downloadJsonCached(url, 10000)
|
||||
if (rawData === undefined) {
|
||||
return undefined
|
||||
}
|
||||
// A full query might contain more then just the requested object (e.g. nodes that are part of a way, where we only want the way)
|
||||
const parsed = OsmObject.ParseObjects(rawData.elements);
|
||||
const parsed = OsmObject.ParseObjects(rawData.elements)
|
||||
// Lets fetch the object we need
|
||||
for (const osmObject of parsed) {
|
||||
if (osmObject.type !== type) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (osmObject.id !== idN) {
|
||||
continue
|
||||
|
@ -97,25 +102,23 @@ export abstract class OsmObject {
|
|||
return osmObject
|
||||
}
|
||||
throw "PANIC: requested object is not part of the response"
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Downloads the ways that are using this node.
|
||||
* Beware: their geometry will be incomplete!
|
||||
*/
|
||||
public static DownloadReferencingWays(id: string): Promise<OsmWay[]> {
|
||||
return Utils.downloadJsonCached(`${OsmObject.backendURL}api/0.6/${id}/ways`, 60 * 1000).then(
|
||||
data => {
|
||||
return data.elements.map(wayInfo => {
|
||||
const way = new OsmWay(wayInfo.id)
|
||||
way.LoadData(wayInfo)
|
||||
return way
|
||||
})
|
||||
}
|
||||
)
|
||||
return Utils.downloadJsonCached(
|
||||
`${OsmObject.backendURL}api/0.6/${id}/ways`,
|
||||
60 * 1000
|
||||
).then((data) => {
|
||||
return data.elements.map((wayInfo) => {
|
||||
const way = new OsmWay(wayInfo.id)
|
||||
way.LoadData(wayInfo)
|
||||
return way
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -123,8 +126,11 @@ export abstract class OsmObject {
|
|||
* Beware: their geometry will be incomplete!
|
||||
*/
|
||||
public static async DownloadReferencingRelations(id: string): Promise<OsmRelation[]> {
|
||||
const data = await Utils.downloadJsonCached(`${OsmObject.backendURL}api/0.6/${id}/relations`, 60 * 1000)
|
||||
return data.elements.map(wayInfo => {
|
||||
const data = await Utils.downloadJsonCached(
|
||||
`${OsmObject.backendURL}api/0.6/${id}/relations`,
|
||||
60 * 1000
|
||||
)
|
||||
return data.elements.map((wayInfo) => {
|
||||
const rel = new OsmRelation(wayInfo.id)
|
||||
rel.LoadData(wayInfo)
|
||||
rel.SaveExtraData(wayInfo, undefined)
|
||||
|
@ -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)) {
|
||||
return OsmObject.historyCache.get(id)
|
||||
}
|
||||
const splitted = id.split("/");
|
||||
const type = splitted[0];
|
||||
const idN = Number(splitted[1]);
|
||||
const src = new UIEventSource<OsmObject[]>([]);
|
||||
OsmObject.historyCache.set(id, src);
|
||||
Utils.downloadJsonCached(`${OsmObject.backendURL}api/0.6/${type}/${idN}/history`, 10 * 60 * 1000).then(data => {
|
||||
const elements: any[] = data.elements;
|
||||
const splitted = id.split("/")
|
||||
const type = splitted[0]
|
||||
const idN = Number(splitted[1])
|
||||
const src = new UIEventSource<OsmObject[]>([])
|
||||
OsmObject.historyCache.set(id, src)
|
||||
Utils.downloadJsonCached(
|
||||
`${OsmObject.backendURL}api/0.6/${type}/${idN}/history`,
|
||||
10 * 60 * 1000
|
||||
).then((data) => {
|
||||
const elements: any[] = data.elements
|
||||
const osmObjects: OsmObject[] = []
|
||||
for (const element of elements) {
|
||||
let osmObject: OsmObject = null
|
||||
switch (type) {
|
||||
case("node"):
|
||||
osmObject = new OsmNode(idN);
|
||||
break;
|
||||
case("way"):
|
||||
osmObject = new OsmWay(idN);
|
||||
break;
|
||||
case("relation"):
|
||||
osmObject = new OsmRelation(idN);
|
||||
break;
|
||||
case "node":
|
||||
osmObject = new OsmNode(idN)
|
||||
break
|
||||
case "way":
|
||||
osmObject = new OsmWay(idN)
|
||||
break
|
||||
case "relation":
|
||||
osmObject = new OsmRelation(idN)
|
||||
break
|
||||
}
|
||||
osmObject?.LoadData(element);
|
||||
osmObject?.SaveExtraData(element, []);
|
||||
osmObject?.LoadData(element)
|
||||
osmObject?.SaveExtraData(element, [])
|
||||
osmObjects.push(osmObject)
|
||||
}
|
||||
src.setData(osmObjects)
|
||||
})
|
||||
return src;
|
||||
return src
|
||||
}
|
||||
|
||||
// bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
|
||||
public static async LoadArea(bbox: BBox): Promise<OsmObject[]> {
|
||||
const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}`
|
||||
const data = await Utils.downloadJson(url)
|
||||
const elements: any[] = data.elements;
|
||||
return OsmObject.ParseObjects(elements);
|
||||
const elements: any[] = data.elements
|
||||
return OsmObject.ParseObjects(elements)
|
||||
}
|
||||
|
||||
public static ParseObjects(elements: any[]): OsmObject[] {
|
||||
const objects: OsmObject[] = [];
|
||||
const objects: OsmObject[] = []
|
||||
const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>()
|
||||
|
||||
for (const element of elements) {
|
||||
const type = element.type;
|
||||
const idN = element.id;
|
||||
const type = element.type
|
||||
const idN = element.id
|
||||
let osmObject: OsmObject = null
|
||||
switch (type) {
|
||||
case("node"):
|
||||
const node = new OsmNode(idN);
|
||||
allNodes.set(idN, node);
|
||||
case "node":
|
||||
const node = new OsmNode(idN)
|
||||
allNodes.set(idN, node)
|
||||
osmObject = node
|
||||
node.SaveExtraData(element);
|
||||
break;
|
||||
case("way"):
|
||||
osmObject = new OsmWay(idN);
|
||||
const nodes = element.nodes.map(i => allNodes.get(i));
|
||||
node.SaveExtraData(element)
|
||||
break
|
||||
case "way":
|
||||
osmObject = new OsmWay(idN)
|
||||
const nodes = element.nodes.map((i) => allNodes.get(i))
|
||||
osmObject.SaveExtraData(element, nodes)
|
||||
break;
|
||||
case("relation"):
|
||||
osmObject = new OsmRelation(idN);
|
||||
const allGeojsons = OsmToGeoJson.default({elements},
|
||||
break
|
||||
case "relation":
|
||||
osmObject = new OsmRelation(idN)
|
||||
const allGeojsons = OsmToGeoJson.default(
|
||||
{ elements },
|
||||
// @ts-ignore
|
||||
{
|
||||
flatProperties: true
|
||||
});
|
||||
const feature = allGeojsons.features.find(f => f.id === osmObject.type + "/" + osmObject.id)
|
||||
flatProperties: true,
|
||||
}
|
||||
)
|
||||
const feature = allGeojsons.features.find(
|
||||
(f) => f.id === osmObject.type + "/" + osmObject.id
|
||||
)
|
||||
osmObject.SaveExtraData(element, feature)
|
||||
break;
|
||||
break
|
||||
}
|
||||
|
||||
if (osmObject !== undefined && OsmObject.backendURL !== OsmObject.defaultBackend) {
|
||||
|
@ -213,12 +226,12 @@ export abstract class OsmObject {
|
|||
osmObject?.LoadData(element)
|
||||
objects.push(osmObject)
|
||||
}
|
||||
return objects;
|
||||
return objects
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the list of polygon features to determine if the given tags are a polygon or not.
|
||||
*
|
||||
*
|
||||
* OsmObject.isPolygon({"building":"yes"}) // => true
|
||||
* OsmObject.isPolygon({"highway":"residential"}) // => false
|
||||
* */
|
||||
|
@ -227,11 +240,12 @@ export abstract class OsmObject {
|
|||
if (!tags.hasOwnProperty(tagsKey)) {
|
||||
continue
|
||||
}
|
||||
const polyGuide: { values: Set<string>; blacklist: boolean } = OsmObject.polygonFeatures.get(tagsKey)
|
||||
const polyGuide: { values: Set<string>; blacklist: boolean } =
|
||||
OsmObject.polygonFeatures.get(tagsKey)
|
||||
if (polyGuide === undefined) {
|
||||
continue
|
||||
}
|
||||
if ((polyGuide.values === null)) {
|
||||
if (polyGuide.values === null) {
|
||||
// .values is null, thus merely _having_ this key is enough to be a polygon (or if blacklist, being a line)
|
||||
return !polyGuide.blacklist
|
||||
}
|
||||
|
@ -243,156 +257,178 @@ export abstract class OsmObject {
|
|||
return doesMatch
|
||||
}
|
||||
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
private static constructPolygonFeatures(): Map<string, { values: Set<string>, blacklist: boolean }> {
|
||||
const result = new Map<string, { values: Set<string>, blacklist: boolean }>();
|
||||
for (const polygonFeature of (polygon_features["default"] ?? polygon_features)) {
|
||||
const key = polygonFeature.key;
|
||||
private static constructPolygonFeatures(): Map<
|
||||
string,
|
||||
{ values: Set<string>; blacklist: boolean }
|
||||
> {
|
||||
const result = new Map<string, { values: Set<string>; blacklist: boolean }>()
|
||||
for (const polygonFeature of polygon_features["default"] ?? polygon_features) {
|
||||
const key = polygonFeature.key
|
||||
|
||||
if (polygonFeature.polygon === "all") {
|
||||
result.set(key, {values: null, blacklist: false})
|
||||
result.set(key, { values: null, blacklist: false })
|
||||
continue
|
||||
}
|
||||
|
||||
const blacklist = polygonFeature.polygon === "blacklist"
|
||||
result.set(key, {values: new Set<string>(polygonFeature.values), blacklist: blacklist})
|
||||
|
||||
result.set(key, {
|
||||
values: new Set<string>(polygonFeature.values),
|
||||
blacklist: blacklist,
|
||||
})
|
||||
}
|
||||
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
// The centerpoint of the feature, as [lat, lon]
|
||||
public abstract centerpoint(): [number, number];
|
||||
public abstract centerpoint(): [number, number]
|
||||
|
||||
public abstract asGeoJson(): any;
|
||||
public abstract asGeoJson(): any
|
||||
|
||||
abstract SaveExtraData(element: any, allElements: OsmObject[] | any);
|
||||
abstract SaveExtraData(element: any, allElements: OsmObject[] | any)
|
||||
|
||||
/**
|
||||
* Generates the changeset-XML for tags
|
||||
* @constructor
|
||||
*/
|
||||
TagsXML(): string {
|
||||
let tags = "";
|
||||
let tags = ""
|
||||
for (const key in this.tags) {
|
||||
if (key.startsWith("_")) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (key === "id") {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
const v = this.tags[key];
|
||||
const v = this.tags[key]
|
||||
if (v !== "" && v !== undefined) {
|
||||
tags += ' <tag k="' + Utils.EncodeXmlValue(key) + '" v="' + Utils.EncodeXmlValue(this.tags[key]) + '"/>\n'
|
||||
tags +=
|
||||
' <tag k="' +
|
||||
Utils.EncodeXmlValue(key) +
|
||||
'" v="' +
|
||||
Utils.EncodeXmlValue(this.tags[key]) +
|
||||
'"/>\n'
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
return tags
|
||||
}
|
||||
|
||||
abstract ChangesetXML(changesetId: string): string;
|
||||
abstract ChangesetXML(changesetId: string): string
|
||||
|
||||
protected VersionXML() {
|
||||
if (this.version === undefined) {
|
||||
return "";
|
||||
return ""
|
||||
}
|
||||
return 'version="' + this.version + '"';
|
||||
return 'version="' + this.version + '"'
|
||||
}
|
||||
|
||||
private LoadData(element: any): void {
|
||||
this.tags = element.tags ?? this.tags;
|
||||
this.version = element.version;
|
||||
this.timestamp = element.timestamp;
|
||||
const tgs = this.tags;
|
||||
this.tags = element.tags ?? this.tags
|
||||
this.version = element.version
|
||||
this.timestamp = element.timestamp
|
||||
const tgs = this.tags
|
||||
if (element.tags === undefined) {
|
||||
// Simple node which is part of a way - not important
|
||||
return;
|
||||
return
|
||||
}
|
||||
tgs["_last_edit:contributor"] = element.user
|
||||
tgs["_last_edit:contributor:uid"] = element.uid
|
||||
tgs["_last_edit:changeset"] = element.changeset
|
||||
tgs["_last_edit:timestamp"] = element.timestamp
|
||||
tgs["_version_number"] = element.version
|
||||
tgs["id"] = this.type + "/" + this.id;
|
||||
tgs["id"] = <OsmId>(this.type + "/" + this.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class OsmNode extends OsmObject {
|
||||
|
||||
lat: number;
|
||||
lon: number;
|
||||
lat: number
|
||||
lon: number
|
||||
|
||||
constructor(id: number) {
|
||||
super("node", id);
|
||||
|
||||
super("node", id)
|
||||
}
|
||||
|
||||
ChangesetXML(changesetId: string): string {
|
||||
let tags = this.TagsXML();
|
||||
let tags = this.TagsXML()
|
||||
|
||||
return ' <node id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + ' lat="' + this.lat + '" lon="' + this.lon + '">\n' +
|
||||
return (
|
||||
' <node id="' +
|
||||
this.id +
|
||||
'" changeset="' +
|
||||
changesetId +
|
||||
'" ' +
|
||||
this.VersionXML() +
|
||||
' lat="' +
|
||||
this.lat +
|
||||
'" lon="' +
|
||||
this.lon +
|
||||
'">\n' +
|
||||
tags +
|
||||
' </node>\n';
|
||||
" </node>\n"
|
||||
)
|
||||
}
|
||||
|
||||
SaveExtraData(element) {
|
||||
this.lat = element.lat;
|
||||
this.lon = element.lon;
|
||||
this.lat = element.lat
|
||||
this.lon = element.lon
|
||||
}
|
||||
|
||||
centerpoint(): [number, number] {
|
||||
return [this.lat, this.lon];
|
||||
return [this.lat, this.lon]
|
||||
}
|
||||
|
||||
asGeoJson() {
|
||||
asGeoJson(): OsmFeature {
|
||||
return {
|
||||
"type": "Feature",
|
||||
"properties": this.tags,
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
this.lon,
|
||||
this.lat
|
||||
]
|
||||
}
|
||||
type: "Feature",
|
||||
properties: this.tags,
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [this.lon, this.lat],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OsmWay extends OsmObject {
|
||||
|
||||
nodes: number[] = [];
|
||||
nodes: number[] = []
|
||||
// The coordinates of the way, [lat, lon][]
|
||||
coordinates: [number, number][] = []
|
||||
lat: number;
|
||||
lon: number;
|
||||
lat: number
|
||||
lon: number
|
||||
|
||||
constructor(id: number) {
|
||||
super("way", id);
|
||||
super("way", id)
|
||||
}
|
||||
|
||||
centerpoint(): [number, number] {
|
||||
return [this.lat, this.lon];
|
||||
return [this.lat, this.lon]
|
||||
}
|
||||
|
||||
ChangesetXML(changesetId: string): string {
|
||||
let tags = this.TagsXML();
|
||||
let nds = "";
|
||||
let tags = this.TagsXML()
|
||||
let nds = ""
|
||||
for (const node in this.nodes) {
|
||||
nds += ' <nd ref="' + this.nodes[node] + '"/>\n';
|
||||
nds += ' <nd ref="' + this.nodes[node] + '"/>\n'
|
||||
}
|
||||
|
||||
return ' <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
|
||||
return (
|
||||
' <way id="' +
|
||||
this.id +
|
||||
'" changeset="' +
|
||||
changesetId +
|
||||
'" ' +
|
||||
this.VersionXML() +
|
||||
">\n" +
|
||||
nds +
|
||||
tags +
|
||||
' </way>\n';
|
||||
" </way>\n"
|
||||
)
|
||||
}
|
||||
|
||||
SaveExtraData(element, allNodes: OsmNode[]) {
|
||||
|
||||
let latSum = 0
|
||||
let lonSum = 0
|
||||
|
||||
|
@ -410,88 +446,96 @@ export class OsmWay extends OsmObject {
|
|||
if (node === undefined) {
|
||||
console.error("Error: node ", nodeId, "not found in ", nodeDict)
|
||||
// This is probably part of a relation which hasn't been fully downloaded
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
this.coordinates.push(node.centerpoint());
|
||||
this.coordinates.push(node.centerpoint())
|
||||
latSum += node.lat
|
||||
lonSum += node.lon
|
||||
}
|
||||
let count = this.coordinates.length;
|
||||
this.lat = latSum / count;
|
||||
this.lon = lonSum / count;
|
||||
this.nodes = element.nodes;
|
||||
let count = this.coordinates.length
|
||||
this.lat = latSum / count
|
||||
this.lon = lonSum / count
|
||||
this.nodes = element.nodes
|
||||
}
|
||||
|
||||
public asGeoJson() {
|
||||
let coordinates: ([number, number][] | [number, number][][]) = this.coordinates.map(([lat, lon]) => [lon, lat]);
|
||||
let coordinates: [number, number][] | [number, number][][] = this.coordinates.map(
|
||||
([lat, lon]) => [lon, lat]
|
||||
)
|
||||
if (this.isPolygon()) {
|
||||
coordinates = [coordinates]
|
||||
}
|
||||
return {
|
||||
"type": "Feature",
|
||||
"properties": this.tags,
|
||||
"geometry": {
|
||||
"type": this.isPolygon() ? "Polygon" : "LineString",
|
||||
"coordinates": coordinates
|
||||
}
|
||||
type: "Feature",
|
||||
properties: this.tags,
|
||||
geometry: {
|
||||
type: this.isPolygon() ? "Polygon" : "LineString",
|
||||
coordinates: coordinates,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private isPolygon(): boolean {
|
||||
// Compare lat and lon seperately, as the coordinate array might not be a reference to the same object
|
||||
if (this.coordinates[0][0] !== this.coordinates[this.coordinates.length - 1][0] ||
|
||||
this.coordinates[0][1] !== this.coordinates[this.coordinates.length - 1][1]) {
|
||||
return false; // Not closed
|
||||
if (
|
||||
this.coordinates[0][0] !== this.coordinates[this.coordinates.length - 1][0] ||
|
||||
this.coordinates[0][1] !== this.coordinates[this.coordinates.length - 1][1]
|
||||
) {
|
||||
return false // Not closed
|
||||
}
|
||||
return OsmObject.isPolygon(this.tags)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class OsmRelation extends OsmObject {
|
||||
|
||||
public members: {
|
||||
type: "node" | "way" | "relation",
|
||||
ref: number,
|
||||
type: "node" | "way" | "relation"
|
||||
ref: number
|
||||
role: string
|
||||
}[];
|
||||
}[]
|
||||
|
||||
private geojson = undefined
|
||||
|
||||
constructor(id: number) {
|
||||
super("relation", id);
|
||||
super("relation", id)
|
||||
}
|
||||
|
||||
centerpoint(): [number, number] {
|
||||
return [0, 0]; // TODO
|
||||
return [0, 0] // TODO
|
||||
}
|
||||
|
||||
ChangesetXML(changesetId: string): string {
|
||||
let members = "";
|
||||
let members = ""
|
||||
for (const member of this.members) {
|
||||
members += ' <member type="' + member.type + '" ref="' + member.ref + '" role="' + member.role + '"/>\n';
|
||||
members +=
|
||||
' <member type="' +
|
||||
member.type +
|
||||
'" ref="' +
|
||||
member.ref +
|
||||
'" role="' +
|
||||
member.role +
|
||||
'"/>\n'
|
||||
}
|
||||
|
||||
let tags = this.TagsXML();
|
||||
let tags = this.TagsXML()
|
||||
let cs = ""
|
||||
if (changesetId !== undefined) {
|
||||
cs = `changeset="${changesetId}"`
|
||||
}
|
||||
return ` <relation id="${this.id}" ${cs} ${this.VersionXML()}>
|
||||
${members}${tags} </relation>
|
||||
`;
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
SaveExtraData(element, geojson) {
|
||||
this.members = element.members;
|
||||
this.members = element.members
|
||||
this.geojson = geojson
|
||||
}
|
||||
|
||||
asGeoJson(): any {
|
||||
if (this.geojson !== undefined) {
|
||||
return this.geojson;
|
||||
return this.geojson
|
||||
}
|
||||
throw "Not Implemented"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import UserDetails, {OsmConnection} from "./OsmConnection";
|
||||
import {Utils} from "../../Utils";
|
||||
import {DomEvent} from "leaflet";
|
||||
import preventDefault = DomEvent.preventDefault;
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import UserDetails, { OsmConnection } from "./OsmConnection"
|
||||
import { Utils } from "../../Utils"
|
||||
import { DomEvent } from "leaflet"
|
||||
import preventDefault = DomEvent.preventDefault
|
||||
|
||||
export class OsmPreferences {
|
||||
|
||||
public preferences = new UIEventSource<Record<string, string>>({}, "all-osm-preferences");
|
||||
public preferences = new UIEventSource<Record<string, string>>({}, "all-osm-preferences")
|
||||
private readonly preferenceSources = new Map<string, UIEventSource<string>>()
|
||||
private auth: any;
|
||||
private userDetails: UIEventSource<UserDetails>;
|
||||
private longPreferences = {};
|
||||
private auth: any
|
||||
private userDetails: UIEventSource<UserDetails>
|
||||
private longPreferences = {}
|
||||
|
||||
constructor(auth, osmConnection: OsmConnection) {
|
||||
this.auth = auth;
|
||||
this.userDetails = osmConnection.userDetails;
|
||||
const self = this;
|
||||
osmConnection.OnLoggedIn(() => self.UpdatePreferences());
|
||||
this.auth = auth
|
||||
this.userDetails = osmConnection.userDetails
|
||||
const self = this
|
||||
osmConnection.OnLoggedIn(() => self.UpdatePreferences())
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -26,42 +25,44 @@ export class OsmPreferences {
|
|||
* @constructor
|
||||
*/
|
||||
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||
|
||||
if (this.longPreferences[prefix + key] !== undefined) {
|
||||
return this.longPreferences[prefix + key];
|
||||
return this.longPreferences[prefix + key]
|
||||
}
|
||||
|
||||
const source = new UIEventSource<string>(undefined, "long-osm-preference:" + prefix + key)
|
||||
this.longPreferences[prefix + key] = source
|
||||
|
||||
const source = new UIEventSource<string>(undefined, "long-osm-preference:" + prefix + key);
|
||||
this.longPreferences[prefix + key] = source;
|
||||
|
||||
const allStartWith = prefix + key + "-combined";
|
||||
const allStartWith = prefix + key + "-combined"
|
||||
// Gives the number of combined preferences
|
||||
const length = this.GetPreference(allStartWith + "-length", "", "");
|
||||
const length = this.GetPreference(allStartWith + "-length", "", "")
|
||||
|
||||
if( (allStartWith + "-length").length > 255){
|
||||
throw "This preference key is too long, it has "+key.length+" characters, but at most "+(255 - "-length".length - "-combined".length - prefix.length)+" characters are allowed"
|
||||
}
|
||||
if ((allStartWith + "-length").length > 255) {
|
||||
throw (
|
||||
"This preference key is too long, it has " +
|
||||
key.length +
|
||||
" characters, but at most " +
|
||||
(255 - "-length".length - "-combined".length - prefix.length) +
|
||||
" characters are allowed"
|
||||
)
|
||||
}
|
||||
|
||||
const self = this;
|
||||
source.addCallback(str => {
|
||||
const self = this
|
||||
source.addCallback((str) => {
|
||||
if (str === undefined || str === "") {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (str === null) {
|
||||
console.error("Deleting " + allStartWith);
|
||||
let count = parseInt(length.data);
|
||||
console.error("Deleting " + allStartWith)
|
||||
let count = parseInt(length.data)
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Delete all the preferences
|
||||
self.GetPreference(allStartWith + "-" + i, "", "")
|
||||
.setData("");
|
||||
self.GetPreference(allStartWith + "-" + i, "", "").setData("")
|
||||
}
|
||||
self.GetPreference(allStartWith + "-length", "", "")
|
||||
.setData("");
|
||||
self.GetPreference(allStartWith + "-length", "", "").setData("")
|
||||
return
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
let i = 0
|
||||
while (str !== "") {
|
||||
if (str === undefined || str === "undefined") {
|
||||
throw "Long pref became undefined?"
|
||||
|
@ -69,79 +70,91 @@ export class OsmPreferences {
|
|||
if (i > 100) {
|
||||
throw "This long preference is getting very long... "
|
||||
}
|
||||
self.GetPreference(allStartWith + "-" + i, "","").setData(str.substr(0, 255));
|
||||
str = str.substr(255);
|
||||
i++;
|
||||
self.GetPreference(allStartWith + "-" + i, "", "").setData(str.substr(0, 255))
|
||||
str = str.substr(255)
|
||||
i++
|
||||
}
|
||||
length.setData("" + i); // We use I, the number of preference fields used
|
||||
});
|
||||
|
||||
|
||||
function updateData(l: number) {
|
||||
if(Object.keys(self.preferences.data).length === 0){
|
||||
// The preferences are still empty - they are not yet updated, so we delay updating for now
|
||||
return
|
||||
}
|
||||
const prefsCount = Number(l);
|
||||
if (prefsCount > 100) {
|
||||
throw "Length to long";
|
||||
}
|
||||
let str = "";
|
||||
for (let i = 0; i < prefsCount; i++) {
|
||||
const key = allStartWith + "-" + i
|
||||
if(self.preferences.data[key] === undefined){
|
||||
console.warn("Detected a broken combined preference:", key, "is undefined", self.preferences)
|
||||
}
|
||||
str += self.preferences.data[key] ?? "";
|
||||
}
|
||||
|
||||
source.setData(str);
|
||||
}
|
||||
|
||||
length.addCallback(l => {
|
||||
updateData(Number(l));
|
||||
});
|
||||
this.preferences.addCallbackAndRun(_ => {
|
||||
updateData(Number(length.data));
|
||||
length.setData("" + i) // We use I, the number of preference fields used
|
||||
})
|
||||
|
||||
return source;
|
||||
function updateData(l: number) {
|
||||
if (Object.keys(self.preferences.data).length === 0) {
|
||||
// The preferences are still empty - they are not yet updated, so we delay updating for now
|
||||
return
|
||||
}
|
||||
const prefsCount = Number(l)
|
||||
if (prefsCount > 100) {
|
||||
throw "Length to long"
|
||||
}
|
||||
let str = ""
|
||||
for (let i = 0; i < prefsCount; i++) {
|
||||
const key = allStartWith + "-" + i
|
||||
if (self.preferences.data[key] === undefined) {
|
||||
console.warn(
|
||||
"Detected a broken combined preference:",
|
||||
key,
|
||||
"is undefined",
|
||||
self.preferences
|
||||
)
|
||||
}
|
||||
str += self.preferences.data[key] ?? ""
|
||||
}
|
||||
|
||||
source.setData(str)
|
||||
}
|
||||
|
||||
length.addCallback((l) => {
|
||||
updateData(Number(l))
|
||||
})
|
||||
this.preferences.addCallbackAndRun((_) => {
|
||||
updateData(Number(length.data))
|
||||
})
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
public GetPreference(key: string, defaultValue : string = undefined, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||
if(key.startsWith(prefix) && prefix !== ""){
|
||||
console.trace("A preference was requested which has a duplicate prefix in its key. This is probably a bug")
|
||||
public GetPreference(
|
||||
key: string,
|
||||
defaultValue: string = undefined,
|
||||
prefix: string = "mapcomplete-"
|
||||
): UIEventSource<string> {
|
||||
if (key.startsWith(prefix) && prefix !== "") {
|
||||
console.trace(
|
||||
"A preference was requested which has a duplicate prefix in its key. This is probably a bug"
|
||||
)
|
||||
}
|
||||
key = prefix + key;
|
||||
key = key.replace(/[:\\\/"' {}.%]/g, '')
|
||||
key = prefix + key
|
||||
key = key.replace(/[:\\\/"' {}.%]/g, "")
|
||||
if (key.length >= 255) {
|
||||
throw "Preferences: key length to big";
|
||||
throw "Preferences: key length to big"
|
||||
}
|
||||
const cached = this.preferenceSources.get(key)
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
return cached
|
||||
}
|
||||
if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) {
|
||||
this.UpdatePreferences();
|
||||
this.UpdatePreferences()
|
||||
}
|
||||
|
||||
const pref = new UIEventSource<string>(this.preferences.data[key] ?? defaultValue, "osm-preference:" + key);
|
||||
pref.addCallback((v) => {
|
||||
this.UploadPreference(key, v);
|
||||
});
|
||||
|
||||
|
||||
const pref = new UIEventSource<string>(
|
||||
this.preferences.data[key] ?? defaultValue,
|
||||
"osm-preference:" + key
|
||||
)
|
||||
pref.addCallback((v) => {
|
||||
this.UploadPreference(key, v)
|
||||
})
|
||||
|
||||
this.preferenceSources.set(key, pref)
|
||||
return pref;
|
||||
return pref
|
||||
}
|
||||
|
||||
public ClearPreferences() {
|
||||
let isRunning = false;
|
||||
const self = this;
|
||||
this.preferences.addCallback(prefs => {
|
||||
let isRunning = false
|
||||
const self = this
|
||||
this.preferences.addCallback((prefs) => {
|
||||
console.log("Cleaning preferences...")
|
||||
if (Object.keys(prefs).length == 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (isRunning) {
|
||||
return
|
||||
|
@ -149,94 +162,98 @@ export class OsmPreferences {
|
|||
isRunning = true
|
||||
const prefixes = ["mapcomplete-"]
|
||||
for (const key in prefs) {
|
||||
const matches = prefixes.some(prefix => key.startsWith(prefix))
|
||||
const matches = prefixes.some((prefix) => key.startsWith(prefix))
|
||||
if (matches) {
|
||||
console.log("Clearing ", key)
|
||||
self.GetPreference(key, "", "").setData("")
|
||||
|
||||
}
|
||||
}
|
||||
isRunning = false;
|
||||
return;
|
||||
isRunning = false
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
private UpdatePreferences() {
|
||||
const self = this;
|
||||
this.auth.xhr({
|
||||
method: 'GET',
|
||||
path: '/api/0.6/user/preferences'
|
||||
}, function (error, value: XMLDocument) {
|
||||
if (error) {
|
||||
console.log("Could not load preferences", error);
|
||||
return;
|
||||
}
|
||||
const prefs = value.getElementsByTagName("preference");
|
||||
for (let i = 0; i < prefs.length; i++) {
|
||||
const pref = prefs[i];
|
||||
const k = pref.getAttribute("k");
|
||||
const v = pref.getAttribute("v");
|
||||
self.preferences.data[k] = v;
|
||||
}
|
||||
|
||||
// We merge all the preferences: new keys are uploaded
|
||||
// For differing values, the server overrides local changes
|
||||
self.preferenceSources.forEach((preference, key) => {
|
||||
const osmValue = self.preferences.data[key]
|
||||
if(osmValue === undefined && preference.data !== undefined){
|
||||
// OSM doesn't know this value yet
|
||||
self.UploadPreference(key, preference.data)
|
||||
} else {
|
||||
// OSM does have a value - set it
|
||||
preference.setData(osmValue)
|
||||
const self = this
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "GET",
|
||||
path: "/api/0.6/user/preferences",
|
||||
},
|
||||
function (error, value: XMLDocument) {
|
||||
if (error) {
|
||||
console.log("Could not load preferences", error)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
self.preferences.ping();
|
||||
});
|
||||
const prefs = value.getElementsByTagName("preference")
|
||||
for (let i = 0; i < prefs.length; i++) {
|
||||
const pref = prefs[i]
|
||||
const k = pref.getAttribute("k")
|
||||
const v = pref.getAttribute("v")
|
||||
self.preferences.data[k] = v
|
||||
}
|
||||
|
||||
// We merge all the preferences: new keys are uploaded
|
||||
// For differing values, the server overrides local changes
|
||||
self.preferenceSources.forEach((preference, key) => {
|
||||
const osmValue = self.preferences.data[key]
|
||||
if (osmValue === undefined && preference.data !== undefined) {
|
||||
// OSM doesn't know this value yet
|
||||
self.UploadPreference(key, preference.data)
|
||||
} else {
|
||||
// OSM does have a value - set it
|
||||
preference.setData(osmValue)
|
||||
}
|
||||
})
|
||||
|
||||
self.preferences.ping()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private UploadPreference(k: string, v: string) {
|
||||
if (!this.userDetails.data.loggedIn) {
|
||||
console.debug(`Not saving preference ${k}: user not logged in`);
|
||||
return;
|
||||
console.debug(`Not saving preference ${k}: user not logged in`)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.preferences.data[k] === v) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15));
|
||||
console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15))
|
||||
|
||||
if (v === undefined || v === "") {
|
||||
this.auth.xhr({
|
||||
method: 'DELETE',
|
||||
path: '/api/0.6/user/preferences/' + encodeURIComponent(k),
|
||||
options: {header: {'Content-Type': 'text/plain'}},
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
console.warn("Could not remove preference", error);
|
||||
return;
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
|
||||
options: { header: { "Content-Type": "text/plain" } },
|
||||
},
|
||||
function (error) {
|
||||
if (error) {
|
||||
console.warn("Could not remove preference", error)
|
||||
return
|
||||
}
|
||||
console.debug("Preference ", k, "removed!")
|
||||
}
|
||||
console.debug("Preference ", k, "removed!");
|
||||
|
||||
});
|
||||
return;
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
this.auth.xhr({
|
||||
method: 'PUT',
|
||||
path: '/api/0.6/user/preferences/' + encodeURIComponent(k),
|
||||
options: {header: {'Content-Type': 'text/plain'}},
|
||||
content: v
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
console.warn(`Could not set preference "${k}"'`, error);
|
||||
return;
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "PUT",
|
||||
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
|
||||
options: { header: { "Content-Type": "text/plain" } },
|
||||
content: v,
|
||||
},
|
||||
function (error) {
|
||||
if (error) {
|
||||
console.warn(`Could not set preference "${k}"'`, error)
|
||||
return
|
||||
}
|
||||
console.debug(`Preference ${k} written!`)
|
||||
}
|
||||
console.debug(`Preference ${k} written!`);
|
||||
});
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,56 +1,67 @@
|
|||
import {TagsFilter} from "../Tags/TagsFilter";
|
||||
import RelationsTracker from "./RelationsTracker";
|
||||
import {Utils} from "../../Utils";
|
||||
import {ImmutableStore, Store} from "../UIEventSource";
|
||||
import {BBox} from "../BBox";
|
||||
import * as osmtogeojson from "osmtogeojson";
|
||||
import {FeatureCollection} from "@turf/turf";
|
||||
import { TagsFilter } from "../Tags/TagsFilter"
|
||||
import RelationsTracker from "./RelationsTracker"
|
||||
import { Utils } from "../../Utils"
|
||||
import { ImmutableStore, Store } from "../UIEventSource"
|
||||
import { BBox } from "../BBox"
|
||||
import * as osmtogeojson from "osmtogeojson"
|
||||
import { FeatureCollection } from "@turf/turf"
|
||||
|
||||
/**
|
||||
* Interfaces overpass to get all the latest data
|
||||
*/
|
||||
export class Overpass {
|
||||
private _filter: TagsFilter
|
||||
private readonly _interpreterUrl: string;
|
||||
private readonly _timeout: Store<number>;
|
||||
private readonly _extraScripts: string[];
|
||||
private _includeMeta: boolean;
|
||||
private _relationTracker: RelationsTracker;
|
||||
private readonly _interpreterUrl: string
|
||||
private readonly _timeout: Store<number>
|
||||
private readonly _extraScripts: string[]
|
||||
private _includeMeta: boolean
|
||||
private _relationTracker: RelationsTracker
|
||||
|
||||
constructor(filter: TagsFilter,
|
||||
extraScripts: string[],
|
||||
interpreterUrl: string,
|
||||
timeout?: Store<number>,
|
||||
relationTracker?: RelationsTracker,
|
||||
includeMeta = true) {
|
||||
this._timeout = timeout ?? new ImmutableStore<number>(90);
|
||||
this._interpreterUrl = interpreterUrl;
|
||||
constructor(
|
||||
filter: TagsFilter,
|
||||
extraScripts: string[],
|
||||
interpreterUrl: string,
|
||||
timeout?: Store<number>,
|
||||
relationTracker?: RelationsTracker,
|
||||
includeMeta = true
|
||||
) {
|
||||
this._timeout = timeout ?? new ImmutableStore<number>(90)
|
||||
this._interpreterUrl = interpreterUrl
|
||||
const optimized = filter.optimize()
|
||||
if(optimized === true || optimized === false){
|
||||
if (optimized === true || optimized === false) {
|
||||
throw "Invalid filter: optimizes to true of false"
|
||||
}
|
||||
this._filter = optimized
|
||||
this._extraScripts = extraScripts;
|
||||
this._includeMeta = includeMeta;
|
||||
this._extraScripts = extraScripts
|
||||
this._includeMeta = includeMeta
|
||||
this._relationTracker = relationTracker
|
||||
}
|
||||
|
||||
public async queryGeoJson(bounds: BBox): Promise<[FeatureCollection, Date]> {
|
||||
const bbox = "[bbox:" + bounds.getSouth() + "," + bounds.getWest() + "," + bounds.getNorth() + "," + bounds.getEast() + "]";
|
||||
const bbox =
|
||||
"[bbox:" +
|
||||
bounds.getSouth() +
|
||||
"," +
|
||||
bounds.getWest() +
|
||||
"," +
|
||||
bounds.getNorth() +
|
||||
"," +
|
||||
bounds.getEast() +
|
||||
"]"
|
||||
const query = this.buildScript(bbox)
|
||||
return this.ExecuteQuery(query);
|
||||
return this.ExecuteQuery(query)
|
||||
}
|
||||
|
||||
public buildUrl(query: string){
|
||||
|
||||
public buildUrl(query: string) {
|
||||
return `${this._interpreterUrl}?data=${encodeURIComponent(query)}`
|
||||
}
|
||||
|
||||
public async ExecuteQuery(query: string):Promise<[FeatureCollection, Date]> {
|
||||
const self = this;
|
||||
|
||||
public async ExecuteQuery(query: string): Promise<[FeatureCollection, Date]> {
|
||||
const self = this
|
||||
const json = await Utils.downloadJson(this.buildUrl(query))
|
||||
|
||||
if (json.elements.length === 0 && json.remark !== undefined) {
|
||||
console.warn("Timeout or other runtime error while querying overpass", json.remark);
|
||||
console.warn("Timeout or other runtime error while querying overpass", json.remark)
|
||||
throw `Runtime error (timeout or similar)${json.remark}`
|
||||
}
|
||||
if (json.elements.length === 0) {
|
||||
|
@ -58,77 +69,81 @@ export class Overpass {
|
|||
}
|
||||
|
||||
self._relationTracker?.RegisterRelations(json)
|
||||
const geojson = osmtogeojson.default(json);
|
||||
const osmTime = new Date(json.osm3s.timestamp_osm_base);
|
||||
return [<any> geojson, osmTime];
|
||||
const geojson = osmtogeojson.default(json)
|
||||
const osmTime = new Date(json.osm3s.timestamp_osm_base)
|
||||
return [<any>geojson, osmTime]
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the actual script to execute on Overpass
|
||||
* 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink'
|
||||
*
|
||||
*
|
||||
* import {Tag} from "../Tags/Tag";
|
||||
*
|
||||
*
|
||||
* new Overpass(new Tag("key","value"), [], "").buildScript("{{bbox}}") // => `[out:json][timeout:90]{{bbox}};(nwr["key"="value"];);out body;out meta;>;out skel qt;`
|
||||
*/
|
||||
public buildScript(bbox: string, postCall: string = "", pretty = false): string {
|
||||
const filters = this._filter.asOverpass()
|
||||
let filter = ""
|
||||
for (const filterOr of filters) {
|
||||
if(pretty){
|
||||
if (pretty) {
|
||||
filter += " "
|
||||
}
|
||||
filter += 'nwr' + filterOr + postCall + ';'
|
||||
if(pretty){
|
||||
filter+="\n"
|
||||
filter += "nwr" + filterOr + postCall + ";"
|
||||
if (pretty) {
|
||||
filter += "\n"
|
||||
}
|
||||
}
|
||||
for (const extraScript of this._extraScripts) {
|
||||
filter += '(' + extraScript + ');';
|
||||
filter += "(" + extraScript + ");"
|
||||
}
|
||||
return`[out:json][timeout:${this._timeout.data}]${bbox};(${filter});out body;${this._includeMeta ? 'out meta;' : ''}>;out skel qt;`
|
||||
return `[out:json][timeout:${this._timeout.data}]${bbox};(${filter});out body;${
|
||||
this._includeMeta ? "out meta;" : ""
|
||||
}>;out skel qt;`
|
||||
}
|
||||
/**
|
||||
* Constructs the actual script to execute on Overpass with geocoding
|
||||
* 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink'
|
||||
*
|
||||
*/
|
||||
public buildScriptInArea(area: {osm_type: "way" | "relation", osm_id: number}, pretty = false): string {
|
||||
public buildScriptInArea(
|
||||
area: { osm_type: "way" | "relation"; osm_id: number },
|
||||
pretty = false
|
||||
): string {
|
||||
const filters = this._filter.asOverpass()
|
||||
let filter = ""
|
||||
for (const filterOr of filters) {
|
||||
if(pretty){
|
||||
if (pretty) {
|
||||
filter += " "
|
||||
}
|
||||
filter += 'nwr' + filterOr + '(area.searchArea);'
|
||||
if(pretty){
|
||||
filter+="\n"
|
||||
filter += "nwr" + filterOr + "(area.searchArea);"
|
||||
if (pretty) {
|
||||
filter += "\n"
|
||||
}
|
||||
}
|
||||
for (const extraScript of this._extraScripts) {
|
||||
filter += '(' + extraScript + ');';
|
||||
filter += "(" + extraScript + ");"
|
||||
}
|
||||
let id = area.osm_id;
|
||||
if(area.osm_type === "relation"){
|
||||
let id = area.osm_id
|
||||
if (area.osm_type === "relation") {
|
||||
id += 3600000000
|
||||
}
|
||||
return`[out:json][timeout:${this._timeout.data}];
|
||||
return `[out:json][timeout:${this._timeout.data}];
|
||||
area(id:${id})->.searchArea;
|
||||
(${filter});
|
||||
out body;${this._includeMeta ? 'out meta;' : ''}>;out skel qt;`
|
||||
out body;${this._includeMeta ? "out meta;" : ""}>;out skel qt;`
|
||||
}
|
||||
|
||||
|
||||
|
||||
public buildQuery(bbox: string) {
|
||||
return this.buildUrl(this.buildScript(bbox))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Little helper method to quickly open overpass-turbo in the browser
|
||||
*/
|
||||
public static AsOverpassTurboLink(tags: TagsFilter){
|
||||
public static AsOverpassTurboLink(tags: TagsFilter) {
|
||||
const overpass = new Overpass(tags, [], "", undefined, undefined, false)
|
||||
const script = overpass.buildScript("","({{bbox}})", true)
|
||||
const script = overpass.buildScript("", "({{bbox}})", true)
|
||||
const url = "http://overpass-turbo.eu/?Q="
|
||||
return url + encodeURIComponent(script)
|
||||
}
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
|
||||
export interface Relation {
|
||||
id: number,
|
||||
id: number
|
||||
type: "relation"
|
||||
members: {
|
||||
type: ("way" | "node" | "relation"),
|
||||
ref: number,
|
||||
type: "way" | "node" | "relation"
|
||||
ref: number
|
||||
role: string
|
||||
}[],
|
||||
tags: any,
|
||||
}[]
|
||||
tags: any
|
||||
// Alias for tags; tags == properties
|
||||
properties: any
|
||||
}
|
||||
|
||||
export default class RelationsTracker {
|
||||
public knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(
|
||||
new Map(),
|
||||
"Relation memberships"
|
||||
)
|
||||
|
||||
public knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(new Map(), "Relation memberships");
|
||||
|
||||
constructor() {
|
||||
}
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Gets an overview of the relations - except for multipolygons. We don't care about those
|
||||
|
@ -26,8 +27,9 @@ export default class RelationsTracker {
|
|||
* @constructor
|
||||
*/
|
||||
private static GetRelationElements(overpassJson: any): Relation[] {
|
||||
const relations = overpassJson.elements
|
||||
.filter(element => element.type === "relation" && element.tags.type !== "multipolygon")
|
||||
const relations = overpassJson.elements.filter(
|
||||
(element) => element.type === "relation" && element.tags.type !== "multipolygon"
|
||||
)
|
||||
for (const relation of relations) {
|
||||
relation.properties = relation.tags
|
||||
}
|
||||
|
@ -45,12 +47,12 @@ export default class RelationsTracker {
|
|||
*/
|
||||
private UpdateMembershipTable(relations: Relation[]): void {
|
||||
const memberships = this.knownRelations.data
|
||||
let changed = false;
|
||||
let changed = false
|
||||
for (const relation of relations) {
|
||||
for (const member of relation.members) {
|
||||
const role = {
|
||||
role: member.role,
|
||||
relation: relation
|
||||
relation: relation,
|
||||
}
|
||||
const key = member.type + "/" + member.ref
|
||||
if (!memberships.has(key)) {
|
||||
|
@ -58,19 +60,17 @@ export default class RelationsTracker {
|
|||
}
|
||||
const knownRelations = memberships.get(key)
|
||||
|
||||
const alreadyExists = knownRelations.some(knownRole => {
|
||||
const alreadyExists = knownRelations.some((knownRole) => {
|
||||
return knownRole.role === role.role && knownRole.relation === role.relation
|
||||
})
|
||||
if (!alreadyExists) {
|
||||
knownRelations.push(role)
|
||||
changed = true;
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.knownRelations.ping()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
export default class AspectedRouting {
|
||||
|
||||
public readonly name: string
|
||||
public readonly description: string
|
||||
public readonly units: string
|
||||
public readonly program: any
|
||||
|
||||
public constructor(program) {
|
||||
this.name = program.name;
|
||||
this.description = program.description;
|
||||
this.name = program.name
|
||||
this.description = program.description
|
||||
this.units = program.unit
|
||||
this.program = JSON.parse(JSON.stringify(program))
|
||||
delete this.program.name
|
||||
|
@ -20,40 +19,41 @@ export default class AspectedRouting {
|
|||
*/
|
||||
public static interpret(program: any, properties: any) {
|
||||
if (typeof program !== "object") {
|
||||
return program;
|
||||
return program
|
||||
}
|
||||
|
||||
let functionName /*: string*/ = undefined;
|
||||
let functionName /*: string*/ = undefined
|
||||
let functionArguments /*: any */ = undefined
|
||||
let otherValues = {}
|
||||
// @ts-ignore
|
||||
Object.entries(program).forEach(tag => {
|
||||
const [key, value] = tag;
|
||||
if (key.startsWith("$")) {
|
||||
functionName = key
|
||||
functionArguments = value
|
||||
} else {
|
||||
otherValues[key] = value
|
||||
}
|
||||
Object.entries(program).forEach((tag) => {
|
||||
const [key, value] = tag
|
||||
if (key.startsWith("$")) {
|
||||
functionName = key
|
||||
functionArguments = value
|
||||
} else {
|
||||
otherValues[key] = value
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
if (functionName === undefined) {
|
||||
return AspectedRouting.interpretAsDictionary(program, properties)
|
||||
}
|
||||
|
||||
if (functionName === '$multiply') {
|
||||
return AspectedRouting.multiplyScore(properties, functionArguments);
|
||||
} else if (functionName === '$firstMatchOf') {
|
||||
return AspectedRouting.getFirstMatchScore(properties, functionArguments);
|
||||
} else if (functionName === '$min') {
|
||||
return AspectedRouting.getMinValue(properties, functionArguments);
|
||||
} else if (functionName === '$max') {
|
||||
return AspectedRouting.getMaxValue(properties, functionArguments);
|
||||
} else if (functionName === '$default') {
|
||||
if (functionName === "$multiply") {
|
||||
return AspectedRouting.multiplyScore(properties, functionArguments)
|
||||
} else if (functionName === "$firstMatchOf") {
|
||||
return AspectedRouting.getFirstMatchScore(properties, functionArguments)
|
||||
} else if (functionName === "$min") {
|
||||
return AspectedRouting.getMinValue(properties, functionArguments)
|
||||
} else if (functionName === "$max") {
|
||||
return AspectedRouting.getMaxValue(properties, functionArguments)
|
||||
} else if (functionName === "$default") {
|
||||
return AspectedRouting.defaultV(functionArguments, otherValues, properties)
|
||||
} else {
|
||||
console.error(`Error: Program ${functionName} is not implemented yet. ${JSON.stringify(program)}`);
|
||||
console.error(
|
||||
`Error: Program ${functionName} is not implemented yet. ${JSON.stringify(program)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,7 @@ export default class AspectedRouting {
|
|||
* surface: {
|
||||
* sett : 0.9
|
||||
* }
|
||||
*
|
||||
*
|
||||
* }
|
||||
*
|
||||
* in combination with the tags {highway: residential},
|
||||
|
@ -86,8 +86,8 @@ export default class AspectedRouting {
|
|||
*/
|
||||
private static interpretAsDictionary(program, tags) {
|
||||
// @ts-ignore
|
||||
return Object.entries(tags).map(tag => {
|
||||
const [key, value] = tag;
|
||||
return Object.entries(tags).map((tag) => {
|
||||
const [key, value] = tag
|
||||
const propertyValue = program[key]
|
||||
if (propertyValue === undefined) {
|
||||
return undefined
|
||||
|
@ -97,7 +97,7 @@ export default class AspectedRouting {
|
|||
}
|
||||
// @ts-ignore
|
||||
return propertyValue[value]
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
private static defaultV(subProgram, otherArgs, tags) {
|
||||
|
@ -105,7 +105,7 @@ export default class AspectedRouting {
|
|||
const normalProgram = Object.entries(otherArgs)[0][1]
|
||||
const value = AspectedRouting.interpret(normalProgram, tags)
|
||||
if (value !== undefined) {
|
||||
return value;
|
||||
return value
|
||||
}
|
||||
return AspectedRouting.interpret(subProgram, tags)
|
||||
}
|
||||
|
@ -121,13 +121,15 @@ export default class AspectedRouting {
|
|||
|
||||
let subResults: any[]
|
||||
if (subprograms.length !== undefined) {
|
||||
subResults = AspectedRouting.concatMap(subprograms, subprogram => AspectedRouting.interpret(subprogram, tags))
|
||||
subResults = AspectedRouting.concatMap(subprograms, (subprogram) =>
|
||||
AspectedRouting.interpret(subprogram, tags)
|
||||
)
|
||||
} else {
|
||||
subResults = AspectedRouting.interpret(subprograms, tags)
|
||||
}
|
||||
|
||||
subResults.filter(r => r !== undefined).forEach(r => number *= parseFloat(r))
|
||||
return number.toFixed(2);
|
||||
subResults.filter((r) => r !== undefined).forEach((r) => (number *= parseFloat(r)))
|
||||
return number.toFixed(2)
|
||||
}
|
||||
|
||||
private static getFirstMatchScore(tags, order: any) {
|
||||
|
@ -136,12 +138,12 @@ export default class AspectedRouting {
|
|||
for (let key of order) {
|
||||
// @ts-ignore
|
||||
for (let entry of Object.entries(JSON.parse(tags))) {
|
||||
const [tagKey, value] = entry;
|
||||
const [tagKey, value] = entry
|
||||
if (key === tagKey) {
|
||||
// We have a match... let's evaluate the subprogram
|
||||
const evaluated = AspectedRouting.interpret(value, tags)
|
||||
if (evaluated !== undefined) {
|
||||
return evaluated;
|
||||
return evaluated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -152,26 +154,30 @@ export default class AspectedRouting {
|
|||
}
|
||||
|
||||
private static getMinValue(tags, subprogram) {
|
||||
const minArr = subprogram.map(part => {
|
||||
if (typeof (part) === 'object') {
|
||||
const calculatedValue = this.interpret(part, tags)
|
||||
return parseFloat(calculatedValue)
|
||||
} else {
|
||||
return parseFloat(part);
|
||||
}
|
||||
}).filter(v => !isNaN(v));
|
||||
return Math.min(...minArr);
|
||||
const minArr = subprogram
|
||||
.map((part) => {
|
||||
if (typeof part === "object") {
|
||||
const calculatedValue = this.interpret(part, tags)
|
||||
return parseFloat(calculatedValue)
|
||||
} else {
|
||||
return parseFloat(part)
|
||||
}
|
||||
})
|
||||
.filter((v) => !isNaN(v))
|
||||
return Math.min(...minArr)
|
||||
}
|
||||
|
||||
private static getMaxValue(tags, subprogram) {
|
||||
const maxArr = subprogram.map(part => {
|
||||
if (typeof (part) === 'object') {
|
||||
return parseFloat(AspectedRouting.interpret(part, tags))
|
||||
} else {
|
||||
return parseFloat(part);
|
||||
}
|
||||
}).filter(v => !isNaN(v));
|
||||
return Math.max(...maxArr);
|
||||
const maxArr = subprogram
|
||||
.map((part) => {
|
||||
if (typeof part === "object") {
|
||||
return parseFloat(AspectedRouting.interpret(part, tags))
|
||||
} else {
|
||||
return parseFloat(part)
|
||||
}
|
||||
})
|
||||
.filter((v) => !isNaN(v))
|
||||
return Math.max(...maxArr)
|
||||
}
|
||||
|
||||
private static concatMap(list, f): any[] {
|
||||
|
@ -185,11 +191,10 @@ export default class AspectedRouting {
|
|||
result.push(elem)
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
public evaluate(properties) {
|
||||
return AspectedRouting.interpret(this.program, properties)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,107 +1,125 @@
|
|||
import {GeoOperations} from "./GeoOperations";
|
||||
import {Utils} from "../Utils";
|
||||
import opening_hours from "opening_hours";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import BaseUIElement from "../UI/BaseUIElement";
|
||||
import Title from "../UI/Base/Title";
|
||||
import {FixedUiElement} from "../UI/Base/FixedUiElement";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import {CountryCoder} from "latlon2country"
|
||||
import Constants from "../Models/Constants";
|
||||
import {TagUtils} from "./Tags/TagUtils";
|
||||
|
||||
import { GeoOperations } from "./GeoOperations"
|
||||
import { Utils } from "../Utils"
|
||||
import opening_hours from "opening_hours"
|
||||
import Combine from "../UI/Base/Combine"
|
||||
import BaseUIElement from "../UI/BaseUIElement"
|
||||
import Title from "../UI/Base/Title"
|
||||
import { FixedUiElement } from "../UI/Base/FixedUiElement"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import { CountryCoder } from "latlon2country"
|
||||
import Constants from "../Models/Constants"
|
||||
import { TagUtils } from "./Tags/TagUtils"
|
||||
|
||||
export class SimpleMetaTagger {
|
||||
public readonly keys: string[];
|
||||
public readonly doc: string;
|
||||
public readonly isLazy: boolean;
|
||||
public readonly keys: string[]
|
||||
public readonly doc: string
|
||||
public readonly isLazy: boolean
|
||||
public readonly includesDates: boolean
|
||||
public readonly applyMetaTagsOnFeature: (feature: any, freshness: Date, layer: LayerConfig, state) => boolean;
|
||||
public readonly applyMetaTagsOnFeature: (
|
||||
feature: any,
|
||||
freshness: Date,
|
||||
layer: LayerConfig,
|
||||
state
|
||||
) => boolean
|
||||
|
||||
/***
|
||||
* A function that adds some extra data to a feature
|
||||
* @param docs: what does this extra data do?
|
||||
* @param f: apply the changes. Returns true if something changed
|
||||
*/
|
||||
constructor(docs: { keys: string[], doc: string, includesDates?: boolean, isLazy?: boolean, cleanupRetagger?: boolean },
|
||||
f: ((feature: any, freshness: Date, layer: LayerConfig, state) => boolean)) {
|
||||
this.keys = docs.keys;
|
||||
this.doc = docs.doc;
|
||||
constructor(
|
||||
docs: {
|
||||
keys: string[]
|
||||
doc: string
|
||||
includesDates?: boolean
|
||||
isLazy?: boolean
|
||||
cleanupRetagger?: boolean
|
||||
},
|
||||
f: (feature: any, freshness: Date, layer: LayerConfig, state) => boolean
|
||||
) {
|
||||
this.keys = docs.keys
|
||||
this.doc = docs.doc
|
||||
this.isLazy = docs.isLazy
|
||||
this.applyMetaTagsOnFeature = f;
|
||||
this.includesDates = docs.includesDates ?? false;
|
||||
this.applyMetaTagsOnFeature = f
|
||||
this.includesDates = docs.includesDates ?? false
|
||||
if (!docs.cleanupRetagger) {
|
||||
for (const key of docs.keys) {
|
||||
if (!key.startsWith('_') && key.toLowerCase().indexOf("theme") < 0) {
|
||||
if (!key.startsWith("_") && key.toLowerCase().indexOf("theme") < 0) {
|
||||
throw `Incorrect key for a calculated meta value '${key}': it should start with underscore (_)`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CountryTagger extends SimpleMetaTagger {
|
||||
private static readonly coder = new CountryCoder(Constants.countryCoderEndpoint, Utils.downloadJson);
|
||||
public runningTasks: Set<any>;
|
||||
private static readonly coder = new CountryCoder(
|
||||
Constants.countryCoderEndpoint,
|
||||
Utils.downloadJson
|
||||
)
|
||||
public runningTasks: Set<any>
|
||||
|
||||
constructor() {
|
||||
const runningTasks = new Set<any>();
|
||||
super
|
||||
(
|
||||
const runningTasks = new Set<any>()
|
||||
super(
|
||||
{
|
||||
keys: ["_country"],
|
||||
doc: "The country code of the property (with latlon2country)",
|
||||
includesDates: false
|
||||
includesDates: false,
|
||||
},
|
||||
((feature, _, __, state) => {
|
||||
let centerPoint: any = GeoOperations.centerpoint(feature);
|
||||
const lat = centerPoint.geometry.coordinates[1];
|
||||
const lon = centerPoint.geometry.coordinates[0];
|
||||
(feature, _, __, state) => {
|
||||
let centerPoint: any = GeoOperations.centerpoint(feature)
|
||||
const lat = centerPoint.geometry.coordinates[1]
|
||||
const lon = centerPoint.geometry.coordinates[0]
|
||||
runningTasks.add(feature)
|
||||
CountryTagger.coder.GetCountryCodeAsync(lon, lat).then(
|
||||
countries => {
|
||||
CountryTagger.coder
|
||||
.GetCountryCodeAsync(lon, lat)
|
||||
.then((countries) => {
|
||||
runningTasks.delete(feature)
|
||||
try {
|
||||
const oldCountry = feature.properties["_country"];
|
||||
feature.properties["_country"] = countries[0].trim().toLowerCase();
|
||||
const oldCountry = feature.properties["_country"]
|
||||
feature.properties["_country"] = countries[0].trim().toLowerCase()
|
||||
if (oldCountry !== feature.properties["_country"]) {
|
||||
const tagsSource = state?.allElements?.getEventSourceById(feature.properties.id);
|
||||
tagsSource?.ping();
|
||||
const tagsSource = state?.allElements?.getEventSourceById(
|
||||
feature.properties.id
|
||||
)
|
||||
tagsSource?.ping()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
).catch(_ => {
|
||||
runningTasks.delete(feature)
|
||||
})
|
||||
return false;
|
||||
})
|
||||
})
|
||||
.catch((_) => {
|
||||
runningTasks.delete(feature)
|
||||
})
|
||||
return false
|
||||
}
|
||||
)
|
||||
this.runningTasks = runningTasks;
|
||||
this.runningTasks = runningTasks
|
||||
}
|
||||
}
|
||||
|
||||
export default class SimpleMetaTaggers {
|
||||
|
||||
public static readonly objectMetaInfo = new SimpleMetaTagger(
|
||||
{
|
||||
keys: ["_last_edit:contributor",
|
||||
keys: [
|
||||
"_last_edit:contributor",
|
||||
"_last_edit:contributor:uid",
|
||||
"_last_edit:changeset",
|
||||
"_last_edit:timestamp",
|
||||
"_version_number",
|
||||
"_backend"],
|
||||
doc: "Information about the last edit of this object."
|
||||
"_backend",
|
||||
],
|
||||
doc: "Information about the last edit of this object.",
|
||||
},
|
||||
(feature) => {/*Note: also called by 'UpdateTagsFromOsmAPI'*/
|
||||
(feature) => {
|
||||
/*Note: also called by 'UpdateTagsFromOsmAPI'*/
|
||||
|
||||
const tgs = feature.properties;
|
||||
const tgs = feature.properties
|
||||
|
||||
function move(src: string, target: string) {
|
||||
if (tgs[src] === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
tgs[target] = tgs[src]
|
||||
delete tgs[src]
|
||||
|
@ -112,7 +130,7 @@ export default class SimpleMetaTaggers {
|
|||
move("changeset", "_last_edit:changeset")
|
||||
move("timestamp", "_last_edit:timestamp")
|
||||
move("version", "_version_number")
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
)
|
||||
public static country = new CountryTagger()
|
||||
|
@ -122,32 +140,45 @@ export default class SimpleMetaTaggers {
|
|||
doc: "Adds the geometry type as property. This is identical to the GoeJson geometry type and is one of `Point`,`LineString`, `Polygon` and exceptionally `MultiPolygon` or `MultiLineString`",
|
||||
},
|
||||
(feature, _) => {
|
||||
const changed = feature.properties["_geometry:type"] === feature.geometry.type;
|
||||
feature.properties["_geometry:type"] = feature.geometry.type;
|
||||
const changed = feature.properties["_geometry:type"] === feature.geometry.type
|
||||
feature.properties["_geometry:type"] = feature.geometry.type
|
||||
return changed
|
||||
}
|
||||
)
|
||||
private static readonly cardinalDirections = {
|
||||
N: 0, NNE: 22.5, NE: 45, ENE: 67.5,
|
||||
E: 90, ESE: 112.5, SE: 135, SSE: 157.5,
|
||||
S: 180, SSW: 202.5, SW: 225, WSW: 247.5,
|
||||
W: 270, WNW: 292.5, NW: 315, NNW: 337.5
|
||||
N: 0,
|
||||
NNE: 22.5,
|
||||
NE: 45,
|
||||
ENE: 67.5,
|
||||
E: 90,
|
||||
ESE: 112.5,
|
||||
SE: 135,
|
||||
SSE: 157.5,
|
||||
S: 180,
|
||||
SSW: 202.5,
|
||||
SW: 225,
|
||||
WSW: 247.5,
|
||||
W: 270,
|
||||
WNW: 292.5,
|
||||
NW: 315,
|
||||
NNW: 337.5,
|
||||
}
|
||||
private static latlon = new SimpleMetaTagger({
|
||||
private static latlon = new SimpleMetaTagger(
|
||||
{
|
||||
keys: ["_lat", "_lon"],
|
||||
doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)"
|
||||
doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)",
|
||||
},
|
||||
(feature => {
|
||||
const centerPoint = GeoOperations.centerpoint(feature);
|
||||
const lat = centerPoint.geometry.coordinates[1];
|
||||
const lon = centerPoint.geometry.coordinates[0];
|
||||
feature.properties["_lat"] = "" + lat;
|
||||
feature.properties["_lon"] = "" + lon;
|
||||
feature._lon = lon; // This is dirty, I know
|
||||
feature._lat = lat;
|
||||
return true;
|
||||
})
|
||||
);
|
||||
(feature) => {
|
||||
const centerPoint = GeoOperations.centerpoint(feature)
|
||||
const lat = centerPoint.geometry.coordinates[1]
|
||||
const lon = centerPoint.geometry.coordinates[0]
|
||||
feature.properties["_lat"] = "" + lat
|
||||
feature.properties["_lon"] = "" + lon
|
||||
feature._lon = lon // This is dirty, I know
|
||||
feature._lat = lat
|
||||
return true
|
||||
}
|
||||
)
|
||||
private static layerInfo = new SimpleMetaTagger(
|
||||
{
|
||||
doc: "The layer-id to which this feature belongs. Note that this might be return any applicable if `passAllFeatures` is defined.",
|
||||
|
@ -156,98 +187,101 @@ export default class SimpleMetaTaggers {
|
|||
},
|
||||
(feature, freshness, layer) => {
|
||||
if (feature.properties._layer === layer.id) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
feature.properties._layer = layer.id
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
)
|
||||
private static noBothButLeftRight = new SimpleMetaTagger(
|
||||
{
|
||||
keys: ["sidewalk:left", "sidewalk:right", "generic_key:left:property", "generic_key:right:property"],
|
||||
keys: [
|
||||
"sidewalk:left",
|
||||
"sidewalk:right",
|
||||
"generic_key:left:property",
|
||||
"generic_key:right:property",
|
||||
],
|
||||
doc: "Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' and 'generic_key:right:property' (and similar for sidewalk tagging). Note that this rewritten tags _will be reuploaded on a change_. To prevent to much unrelated retagging, this is only enabled if the layer has at least some lineRenderings with offset defined",
|
||||
includesDates: false,
|
||||
cleanupRetagger: true
|
||||
cleanupRetagger: true,
|
||||
},
|
||||
((feature, state, layer) => {
|
||||
|
||||
if (!layer.lineRendering.some(lr => lr.leftRightSensitive)) {
|
||||
return;
|
||||
(feature, state, layer) => {
|
||||
if (!layer.lineRendering.some((lr) => lr.leftRightSensitive)) {
|
||||
return
|
||||
}
|
||||
|
||||
return SimpleMetaTaggers.removeBothTagging(feature.properties)
|
||||
})
|
||||
}
|
||||
)
|
||||
private static surfaceArea = new SimpleMetaTagger(
|
||||
{
|
||||
keys: ["_surface", "_surface:ha"],
|
||||
doc: "The surface area of the feature, in square meters and in hectare. Not set on points and ways",
|
||||
isLazy: true
|
||||
isLazy: true,
|
||||
},
|
||||
(feature => {
|
||||
|
||||
(feature) => {
|
||||
Object.defineProperty(feature.properties, "_surface", {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
get: () => {
|
||||
const sqMeters = "" + GeoOperations.surfaceAreaInSqMeters(feature);
|
||||
const sqMeters = "" + GeoOperations.surfaceAreaInSqMeters(feature)
|
||||
delete feature.properties["_surface"]
|
||||
feature.properties["_surface"] = sqMeters;
|
||||
feature.properties["_surface"] = sqMeters
|
||||
return sqMeters
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
Object.defineProperty(feature.properties, "_surface:ha", {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
get: () => {
|
||||
const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature);
|
||||
const sqMetersHa = "" + Math.floor(sqMeters / 1000) / 10;
|
||||
const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature)
|
||||
const sqMetersHa = "" + Math.floor(sqMeters / 1000) / 10
|
||||
delete feature.properties["_surface:ha"]
|
||||
feature.properties["_surface:ha"] = sqMetersHa;
|
||||
feature.properties["_surface:ha"] = sqMetersHa
|
||||
return sqMetersHa
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return true;
|
||||
})
|
||||
);
|
||||
return true
|
||||
}
|
||||
)
|
||||
private static levels = new SimpleMetaTagger(
|
||||
{
|
||||
doc: "Extract the 'level'-tag into a normalized, ';'-separated value",
|
||||
keys: ["_level"]
|
||||
keys: ["_level"],
|
||||
},
|
||||
((feature) => {
|
||||
(feature) => {
|
||||
if (feature.properties["level"] === undefined) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
const l = feature.properties["level"]
|
||||
const newValue = TagUtils.LevelsParser(l).join(";")
|
||||
if(l === newValue) {
|
||||
return false;
|
||||
if (l === newValue) {
|
||||
return false
|
||||
}
|
||||
feature.properties["level"] = newValue
|
||||
return true
|
||||
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
private static canonicalize = new SimpleMetaTagger(
|
||||
{
|
||||
doc: "If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`; `1` will be rewritten to `1m` as well)",
|
||||
keys: ["Theme-defined keys"],
|
||||
|
||||
},
|
||||
((feature, _, __, state) => {
|
||||
const units = Utils.NoNull([].concat(...state?.layoutToUse?.layers?.map(layer => layer.units) ?? []));
|
||||
(feature, _, __, state) => {
|
||||
const units = Utils.NoNull(
|
||||
[].concat(...(state?.layoutToUse?.layers?.map((layer) => layer.units) ?? []))
|
||||
)
|
||||
if (units.length == 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
let rewritten = false;
|
||||
let rewritten = false
|
||||
for (const key in feature.properties) {
|
||||
if (!feature.properties.hasOwnProperty(key)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
for (const unit of units) {
|
||||
if (unit === undefined) {
|
||||
|
@ -258,56 +292,59 @@ export default class SimpleMetaTaggers {
|
|||
continue
|
||||
}
|
||||
if (!unit.appliesToKeys.has(key)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
const value = feature.properties[key]
|
||||
const denom = unit.findDenomination(value, () => feature.properties["_country"])
|
||||
if (denom === undefined) {
|
||||
// no valid value found
|
||||
break;
|
||||
break
|
||||
}
|
||||
const [, denomination] = denom;
|
||||
const defaultDenom = unit.getDefaultDenomination(() => feature.properties["_country"])
|
||||
let canonical = denomination?.canonicalValue(value, defaultDenom == denomination) ?? undefined;
|
||||
const [, denomination] = denom
|
||||
const defaultDenom = unit.getDefaultDenomination(
|
||||
() => feature.properties["_country"]
|
||||
)
|
||||
let canonical =
|
||||
denomination?.canonicalValue(value, defaultDenom == denomination) ??
|
||||
undefined
|
||||
if (canonical === value) {
|
||||
break;
|
||||
break
|
||||
}
|
||||
console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`)
|
||||
if (canonical === undefined && !unit.eraseInvalid) {
|
||||
break;
|
||||
break
|
||||
}
|
||||
|
||||
feature.properties[key] = canonical;
|
||||
rewritten = true;
|
||||
break;
|
||||
feature.properties[key] = canonical
|
||||
rewritten = true
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
return rewritten
|
||||
})
|
||||
}
|
||||
)
|
||||
private static lngth = new SimpleMetaTagger(
|
||||
{
|
||||
keys: ["_length", "_length:km"],
|
||||
doc: "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter"
|
||||
doc: "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter",
|
||||
},
|
||||
(feature => {
|
||||
(feature) => {
|
||||
const l = GeoOperations.lengthInMeters(feature)
|
||||
feature.properties["_length"] = "" + l
|
||||
const km = Math.floor(l / 1000)
|
||||
const kmRest = Math.round((l - km * 1000) / 100)
|
||||
feature.properties["_length:km"] = "" + km + "." + kmRest
|
||||
return true;
|
||||
})
|
||||
return true
|
||||
}
|
||||
)
|
||||
private static isOpen = new SimpleMetaTagger(
|
||||
{
|
||||
keys: ["_isOpen"],
|
||||
doc: "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')",
|
||||
includesDates: true,
|
||||
isLazy: true
|
||||
isLazy: true,
|
||||
},
|
||||
((feature, _, __, state) => {
|
||||
(feature, _, __, state) => {
|
||||
if (Utils.runningFromConsole) {
|
||||
// We are running from console, thus probably creating a cache
|
||||
// isOpen is irrelevant
|
||||
|
@ -315,7 +352,7 @@ export default class SimpleMetaTaggers {
|
|||
}
|
||||
if (feature.properties.opening_hours === "24/7") {
|
||||
feature.properties._isOpen = "yes"
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
// _isOpen is calculated dynamically on every call
|
||||
|
@ -325,92 +362,92 @@ export default class SimpleMetaTaggers {
|
|||
get: () => {
|
||||
const tags = feature.properties
|
||||
if (tags.opening_hours === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (tags._country === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||
const oh = new opening_hours(tags["opening_hours"], {
|
||||
lat: lat,
|
||||
lon: lon,
|
||||
address: {
|
||||
country_code: tags._country.toLowerCase(),
|
||||
state: undefined
|
||||
}
|
||||
}, <any>{tag_key: "opening_hours"});
|
||||
const oh = new opening_hours(
|
||||
tags["opening_hours"],
|
||||
{
|
||||
lat: lat,
|
||||
lon: lon,
|
||||
address: {
|
||||
country_code: tags._country.toLowerCase(),
|
||||
state: undefined,
|
||||
},
|
||||
},
|
||||
<any>{ tag_key: "opening_hours" }
|
||||
)
|
||||
|
||||
// Recalculate!
|
||||
return oh.getState() ? "yes" : "no";
|
||||
|
||||
return oh.getState() ? "yes" : "no"
|
||||
} catch (e) {
|
||||
console.warn("Error while parsing opening hours of ", tags.id, e);
|
||||
console.warn("Error while parsing opening hours of ", tags.id, e)
|
||||
delete tags._isOpen
|
||||
tags["_isOpen"] = "parse_error";
|
||||
tags["_isOpen"] = "parse_error"
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const tagsSource = state.allElements.getEventSourceById(feature.properties.id);
|
||||
|
||||
|
||||
})
|
||||
const tagsSource = state.allElements.getEventSourceById(feature.properties.id)
|
||||
}
|
||||
)
|
||||
private static directionSimplified = new SimpleMetaTagger(
|
||||
{
|
||||
keys: ["_direction:numerical", "_direction:leftright"],
|
||||
doc: "_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map"
|
||||
doc: "_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map",
|
||||
},
|
||||
(feature => {
|
||||
const tags = feature.properties;
|
||||
const direction = tags["camera:direction"] ?? tags["direction"];
|
||||
(feature) => {
|
||||
const tags = feature.properties
|
||||
const direction = tags["camera:direction"] ?? tags["direction"]
|
||||
if (direction === undefined) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
const n = SimpleMetaTaggers.cardinalDirections[direction] ?? Number(direction);
|
||||
const n = SimpleMetaTaggers.cardinalDirections[direction] ?? Number(direction)
|
||||
if (isNaN(n)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
// The % operator has range (-360, 360). We apply a trick to get [0, 360).
|
||||
const normalized = ((n % 360) + 360) % 360;
|
||||
const normalized = ((n % 360) + 360) % 360
|
||||
|
||||
tags["_direction:numerical"] = normalized;
|
||||
tags["_direction:leftright"] = normalized <= 180 ? "right" : "left";
|
||||
return true;
|
||||
})
|
||||
tags["_direction:numerical"] = normalized
|
||||
tags["_direction:leftright"] = normalized <= 180 ? "right" : "left"
|
||||
return true
|
||||
}
|
||||
)
|
||||
private static currentTime = new SimpleMetaTagger(
|
||||
{
|
||||
keys: ["_now:date", "_now:datetime", "_loaded:date", "_loaded:_datetime"],
|
||||
doc: "Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely",
|
||||
includesDates: true
|
||||
includesDates: true,
|
||||
},
|
||||
(feature, freshness) => {
|
||||
const now = new Date();
|
||||
const now = new Date()
|
||||
|
||||
if (typeof freshness === "string") {
|
||||
freshness = new Date(freshness)
|
||||
}
|
||||
|
||||
function date(d: Date) {
|
||||
return d.toISOString().slice(0, 10);
|
||||
return d.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function datetime(d: Date) {
|
||||
return d.toISOString().slice(0, -5).replace("T", " ");
|
||||
return d.toISOString().slice(0, -5).replace("T", " ")
|
||||
}
|
||||
|
||||
feature.properties["_now:date"] = date(now);
|
||||
feature.properties["_now:datetime"] = datetime(now);
|
||||
feature.properties["_loaded:date"] = date(freshness);
|
||||
feature.properties["_loaded:datetime"] = datetime(freshness);
|
||||
return true;
|
||||
feature.properties["_now:date"] = date(now)
|
||||
feature.properties["_now:datetime"] = datetime(now)
|
||||
feature.properties["_loaded:date"] = date(freshness)
|
||||
feature.properties["_loaded:datetime"] = datetime(freshness)
|
||||
return true
|
||||
}
|
||||
);
|
||||
)
|
||||
public static metatags: SimpleMetaTagger[] = [
|
||||
SimpleMetaTaggers.latlon,
|
||||
SimpleMetaTaggers.layerInfo,
|
||||
|
@ -424,11 +461,11 @@ export default class SimpleMetaTaggers {
|
|||
SimpleMetaTaggers.objectMetaInfo,
|
||||
SimpleMetaTaggers.noBothButLeftRight,
|
||||
SimpleMetaTaggers.geometryType,
|
||||
SimpleMetaTaggers.levels
|
||||
|
||||
];
|
||||
public static readonly lazyTags: string[] = [].concat(...SimpleMetaTaggers.metatags.filter(tagger => tagger.isLazy)
|
||||
.map(tagger => tagger.keys));
|
||||
SimpleMetaTaggers.levels,
|
||||
]
|
||||
public static readonly lazyTags: string[] = [].concat(
|
||||
...SimpleMetaTaggers.metatags.filter((tagger) => tagger.isLazy).map((tagger) => tagger.keys)
|
||||
)
|
||||
|
||||
/**
|
||||
* Edits the given object to rewrite 'both'-tagging into a 'left-right' tagging scheme.
|
||||
|
@ -451,36 +488,34 @@ export default class SimpleMetaTaggers {
|
|||
}
|
||||
|
||||
if (tags["sidewalk"]) {
|
||||
|
||||
const v = tags["sidewalk"]
|
||||
switch (v) {
|
||||
case "none":
|
||||
case "no":
|
||||
set("sidewalk:left", "no");
|
||||
set("sidewalk:right", "no");
|
||||
set("sidewalk:left", "no")
|
||||
set("sidewalk:right", "no")
|
||||
break
|
||||
case "both":
|
||||
set("sidewalk:left", "yes");
|
||||
set("sidewalk:right", "yes");
|
||||
break;
|
||||
set("sidewalk:left", "yes")
|
||||
set("sidewalk:right", "yes")
|
||||
break
|
||||
case "left":
|
||||
set("sidewalk:left", "yes");
|
||||
set("sidewalk:right", "no");
|
||||
break;
|
||||
set("sidewalk:left", "yes")
|
||||
set("sidewalk:right", "no")
|
||||
break
|
||||
case "right":
|
||||
set("sidewalk:left", "no");
|
||||
set("sidewalk:right", "yes");
|
||||
break;
|
||||
set("sidewalk:left", "no")
|
||||
set("sidewalk:right", "yes")
|
||||
break
|
||||
default:
|
||||
set("sidewalk:left", v);
|
||||
set("sidewalk:right", v);
|
||||
break;
|
||||
set("sidewalk:left", v)
|
||||
set("sidewalk:right", v)
|
||||
break
|
||||
}
|
||||
delete tags["sidewalk"]
|
||||
somethingChanged = true
|
||||
}
|
||||
|
||||
|
||||
const regex = /\([^:]*\):both:\(.*\)/
|
||||
for (const key in tags) {
|
||||
const v = tags[key]
|
||||
|
@ -503,7 +538,6 @@ export default class SimpleMetaTaggers {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
return somethingChanged
|
||||
}
|
||||
|
||||
|
@ -512,13 +546,16 @@ export default class SimpleMetaTaggers {
|
|||
new Combine([
|
||||
"Metatags are extra tags available, in order to display more data or to give better questions.",
|
||||
"They are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.",
|
||||
"**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object"
|
||||
]).SetClass("flex-col")
|
||||
|
||||
];
|
||||
"**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object",
|
||||
]).SetClass("flex-col"),
|
||||
]
|
||||
|
||||
subElements.push(new Title("Metatags calculated by MapComplete", 2))
|
||||
subElements.push(new FixedUiElement("The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme"))
|
||||
subElements.push(
|
||||
new FixedUiElement(
|
||||
"The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme"
|
||||
)
|
||||
)
|
||||
for (const metatag of SimpleMetaTaggers.metatags) {
|
||||
subElements.push(
|
||||
new Title(metatag.keys.join(", "), 3),
|
||||
|
@ -529,5 +566,4 @@ export default class SimpleMetaTaggers {
|
|||
|
||||
return new Combine(subElements).SetClass("flex-col")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,89 +1,91 @@
|
|||
import FeatureSwitchState from "./FeatureSwitchState";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import {Changes} from "../Osm/Changes";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {BBox} from "../BBox";
|
||||
import {QueryParameters} from "../Web/QueryParameters";
|
||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import ChangeToElementsActor from "../Actors/ChangeToElementsActor";
|
||||
import PendingChangesUploader from "../Actors/PendingChangesUploader";
|
||||
import FeatureSwitchState from "./FeatureSwitchState"
|
||||
import { ElementStorage } from "../ElementStorage"
|
||||
import { Changes } from "../Osm/Changes"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { BBox } from "../BBox"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { Utils } from "../../Utils"
|
||||
import ChangeToElementsActor from "../Actors/ChangeToElementsActor"
|
||||
import PendingChangesUploader from "../Actors/PendingChangesUploader"
|
||||
|
||||
/**
|
||||
* The part of the state keeping track of where the elements, loading them, configuring the feature pipeline etc
|
||||
*/
|
||||
export default class ElementsState extends FeatureSwitchState {
|
||||
|
||||
/**
|
||||
The mapping from id -> UIEventSource<properties>
|
||||
*/
|
||||
public allElements: ElementStorage = new ElementStorage();
|
||||
|
||||
public allElements: ElementStorage = new ElementStorage()
|
||||
|
||||
/**
|
||||
The latest element that was selected
|
||||
*/
|
||||
public readonly selectedElement = new UIEventSource<any>(
|
||||
undefined,
|
||||
"Selected element"
|
||||
);
|
||||
|
||||
public readonly selectedElement = new UIEventSource<any>(undefined, "Selected element")
|
||||
|
||||
/**
|
||||
* The map location: currently centered lat, lon and zoom
|
||||
*/
|
||||
public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl");
|
||||
public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl")
|
||||
|
||||
/**
|
||||
* The current visible extent of the screen
|
||||
*/
|
||||
public readonly currentBounds = new UIEventSource<BBox>(undefined)
|
||||
|
||||
|
||||
constructor(layoutToUse: LayoutConfig) {
|
||||
super(layoutToUse);
|
||||
|
||||
|
||||
function localStorageSynced(key: string, deflt: number, docs: string ): UIEventSource<number>{
|
||||
const localStorage = LocalStorageSource.Get(key)
|
||||
const previousValue = localStorage.data
|
||||
const src = UIEventSource.asFloat(
|
||||
QueryParameters.GetQueryParameter(
|
||||
key,
|
||||
"" + deflt,
|
||||
docs
|
||||
).syncWith(localStorage)
|
||||
);
|
||||
|
||||
if(src.data === deflt){
|
||||
const prev = Number(previousValue)
|
||||
if(!isNaN(prev)){
|
||||
src.setData(prev)
|
||||
}
|
||||
super(layoutToUse)
|
||||
|
||||
function localStorageSynced(
|
||||
key: string,
|
||||
deflt: number,
|
||||
docs: string
|
||||
): UIEventSource<number> {
|
||||
const localStorage = LocalStorageSource.Get(key)
|
||||
const previousValue = localStorage.data
|
||||
const src = UIEventSource.asFloat(
|
||||
QueryParameters.GetQueryParameter(key, "" + deflt, docs).syncWith(localStorage)
|
||||
)
|
||||
|
||||
if (src.data === deflt) {
|
||||
const prev = Number(previousValue)
|
||||
if (!isNaN(prev)) {
|
||||
src.setData(prev)
|
||||
}
|
||||
|
||||
return src;
|
||||
}
|
||||
|
||||
// -- Location control initialization
|
||||
const zoom = localStorageSynced("z",(layoutToUse?.startZoom ?? 1),"The initial/current zoom level")
|
||||
const lat = localStorageSynced("lat",(layoutToUse?.startLat ?? 0),"The initial/current latitude")
|
||||
const lon = localStorageSynced("lon",(layoutToUse?.startLon ?? 0),"The initial/current longitude of the app")
|
||||
return src
|
||||
}
|
||||
|
||||
// -- Location control initialization
|
||||
const zoom = localStorageSynced(
|
||||
"z",
|
||||
layoutToUse?.startZoom ?? 1,
|
||||
"The initial/current zoom level"
|
||||
)
|
||||
const lat = localStorageSynced(
|
||||
"lat",
|
||||
layoutToUse?.startLat ?? 0,
|
||||
"The initial/current latitude"
|
||||
)
|
||||
const lon = localStorageSynced(
|
||||
"lon",
|
||||
layoutToUse?.startLon ?? 0,
|
||||
"The initial/current longitude of the app"
|
||||
)
|
||||
|
||||
this.locationControl.setData({
|
||||
zoom: Utils.asFloat(zoom.data),
|
||||
lat: Utils.asFloat(lat.data),
|
||||
lon: Utils.asFloat(lon.data),
|
||||
})
|
||||
this.locationControl.addCallback((latlonz) => {
|
||||
// Sync the location controls
|
||||
zoom.setData(latlonz.zoom);
|
||||
lat.setData(latlonz.lat);
|
||||
lon.setData(latlonz.lon);
|
||||
});
|
||||
|
||||
|
||||
this.locationControl.setData({
|
||||
zoom: Utils.asFloat(zoom.data),
|
||||
lat: Utils.asFloat(lat.data),
|
||||
lon: Utils.asFloat(lon.data),
|
||||
})
|
||||
this.locationControl.addCallback((latlonz) => {
|
||||
// Sync the location controls
|
||||
zoom.setData(latlonz.zoom)
|
||||
lat.setData(latlonz.lat)
|
||||
lon.setData(latlonz.lon)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +1,39 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import FeaturePipeline from "../FeatureSource/FeaturePipeline";
|
||||
import {Tiles} from "../../Models/TileRange";
|
||||
import ShowDataLayer from "../../UI/ShowDataLayer/ShowDataLayer";
|
||||
import {TileHierarchyAggregator} from "../../UI/ShowDataLayer/TileHierarchyAggregator";
|
||||
import ShowTileInfo from "../../UI/ShowDataLayer/ShowTileInfo";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import MapState from "./MapState";
|
||||
import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler";
|
||||
import Hash from "../Web/Hash";
|
||||
import {BBox} from "../BBox";
|
||||
import FeatureInfoBox from "../../UI/Popup/FeatureInfoBox";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource";
|
||||
import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator";
|
||||
import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import FeaturePipeline from "../FeatureSource/FeaturePipeline"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
import ShowDataLayer from "../../UI/ShowDataLayer/ShowDataLayer"
|
||||
import { TileHierarchyAggregator } from "../../UI/ShowDataLayer/TileHierarchyAggregator"
|
||||
import ShowTileInfo from "../../UI/ShowDataLayer/ShowTileInfo"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import MapState from "./MapState"
|
||||
import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"
|
||||
import Hash from "../Web/Hash"
|
||||
import { BBox } from "../BBox"
|
||||
import FeatureInfoBox from "../../UI/Popup/FeatureInfoBox"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource"
|
||||
import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator"
|
||||
import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
|
||||
export default class FeaturePipelineState extends MapState {
|
||||
|
||||
/**
|
||||
* The piece of code which fetches data from various sources and shows it on the background map
|
||||
*/
|
||||
public readonly featurePipeline: FeaturePipeline;
|
||||
private readonly featureAggregator: TileHierarchyAggregator;
|
||||
public readonly featurePipeline: FeaturePipeline
|
||||
private readonly featureAggregator: TileHierarchyAggregator
|
||||
private readonly metatagRecalculator: MetaTagRecalculator
|
||||
private readonly popups : Map<string, ScrollableFullScreen> = new Map<string, ScrollableFullScreen>();
|
||||
|
||||
private readonly popups: Map<string, ScrollableFullScreen> = new Map<
|
||||
string,
|
||||
ScrollableFullScreen
|
||||
>()
|
||||
|
||||
constructor(layoutToUse: LayoutConfig) {
|
||||
super(layoutToUse);
|
||||
super(layoutToUse)
|
||||
|
||||
const clustering = layoutToUse?.clustering
|
||||
this.featureAggregator = TileHierarchyAggregator.createHierarchy(this);
|
||||
this.featureAggregator = TileHierarchyAggregator.createHierarchy(this)
|
||||
const clusterCounter = this.featureAggregator
|
||||
const self = this;
|
||||
const self = this
|
||||
|
||||
/**
|
||||
* We are a bit in a bind:
|
||||
|
@ -51,26 +53,26 @@ export default class FeaturePipelineState extends MapState {
|
|||
self.metatagRecalculator.registerSource(source)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function registerSource(source: FeatureSourceForLayer & Tiled) {
|
||||
|
||||
function registerSource(source: FeatureSourceForLayer & Tiled) {
|
||||
clusterCounter.addTile(source)
|
||||
const sourceBBox = source.features.map(allFeatures => BBox.bboxAroundAll(allFeatures.map(f => BBox.get(f.feature))))
|
||||
const sourceBBox = source.features.map((allFeatures) =>
|
||||
BBox.bboxAroundAll(allFeatures.map((f) => BBox.get(f.feature)))
|
||||
)
|
||||
|
||||
// Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering
|
||||
const doShowFeatures = source.features.map(
|
||||
f => {
|
||||
(f) => {
|
||||
const z = self.locationControl.data.zoom
|
||||
|
||||
if (!source.layer.isDisplayed.data) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
const bounds = self.currentBounds.data
|
||||
if (bounds === undefined) {
|
||||
// Map is not yet displayed
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
if (!sourceBBox.data.overlapsWith(bounds)) {
|
||||
|
@ -78,10 +80,9 @@ export default class FeaturePipelineState extends MapState {
|
|||
return false
|
||||
}
|
||||
|
||||
|
||||
if (z < source.layer.layerDef.minzoom) {
|
||||
// Layer is always hidden for this zoom level
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
if (z > clustering.maxZoom) {
|
||||
|
@ -93,55 +94,55 @@ export default class FeaturePipelineState extends MapState {
|
|||
return false
|
||||
}
|
||||
|
||||
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex);
|
||||
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex)
|
||||
if (tileZ >= z) {
|
||||
|
||||
while (tileZ > z) {
|
||||
tileZ--
|
||||
tileX = Math.floor(tileX / 2)
|
||||
tileY = Math.floor(tileY / 2)
|
||||
}
|
||||
|
||||
if (clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY))?.totalValue > clustering.minNeededElements) {
|
||||
if (
|
||||
clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY))
|
||||
?.totalValue > clustering.minNeededElements
|
||||
) {
|
||||
// To much elements
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}, [self.currentBounds, source.layer.isDisplayed, sourceBBox]
|
||||
},
|
||||
[self.currentBounds, source.layer.isDisplayed, sourceBBox]
|
||||
)
|
||||
|
||||
new ShowDataLayer(
|
||||
{
|
||||
features: source,
|
||||
leafletMap: self.leafletMap,
|
||||
layerToShow: source.layer.layerDef,
|
||||
doShowLayer: doShowFeatures,
|
||||
selectedElement: self.selectedElement,
|
||||
state: self,
|
||||
popup: (tags, layer) => self.CreatePopup(tags, layer)
|
||||
}
|
||||
)
|
||||
new ShowDataLayer({
|
||||
features: source,
|
||||
leafletMap: self.leafletMap,
|
||||
layerToShow: source.layer.layerDef,
|
||||
doShowLayer: doShowFeatures,
|
||||
selectedElement: self.selectedElement,
|
||||
state: self,
|
||||
popup: (tags, layer) => self.CreatePopup(tags, layer),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
this.featurePipeline = new FeaturePipeline(registerSource, this, {handleRawFeatureSource: registerRaw});
|
||||
this.featurePipeline = new FeaturePipeline(registerSource, this, {
|
||||
handleRawFeatureSource: registerRaw,
|
||||
})
|
||||
this.metatagRecalculator = new MetaTagRecalculator(this, this.featurePipeline)
|
||||
this.metatagRecalculator.registerSource(this.currentView, true)
|
||||
|
||||
sourcesToRegister.forEach(source => self.metatagRecalculator.registerSource(source))
|
||||
sourcesToRegister.forEach((source) => self.metatagRecalculator.registerSource(source))
|
||||
|
||||
new SelectedFeatureHandler(Hash.hash, this)
|
||||
|
||||
this.AddClusteringToMap(this.leafletMap)
|
||||
|
||||
}
|
||||
|
||||
public CreatePopup(tags:UIEventSource<any> , layer: LayerConfig): ScrollableFullScreen{
|
||||
if(this.popups.has(tags.data.id)){
|
||||
return this.popups.get(tags.data.id)
|
||||
|
||||
public CreatePopup(tags: UIEventSource<any>, layer: LayerConfig): ScrollableFullScreen {
|
||||
if (this.popups.has(tags.data.id)) {
|
||||
return this.popups.get(tags.data.id)
|
||||
}
|
||||
const popup = new FeatureInfoBox(tags, layer, this)
|
||||
this.popups.set(tags.data.id, popup)
|
||||
|
@ -155,15 +156,19 @@ export default class FeaturePipelineState extends MapState {
|
|||
*/
|
||||
public AddClusteringToMap(leafletMap: UIEventSource<any>) {
|
||||
const clustering = this.layoutToUse.clustering
|
||||
const self = this;
|
||||
const self = this
|
||||
new ShowDataLayer({
|
||||
features: this.featureAggregator.getCountsForZoom(clustering, this.locationControl, clustering.minNeededElements),
|
||||
features: this.featureAggregator.getCountsForZoom(
|
||||
clustering,
|
||||
this.locationControl,
|
||||
clustering.minNeededElements
|
||||
),
|
||||
leafletMap: leafletMap,
|
||||
layerToShow: ShowTileInfo.styling,
|
||||
popup: this.featureSwitchIsDebugging.data ? (tags, layer) => new FeatureInfoBox(tags, layer, self) : undefined,
|
||||
state: this
|
||||
popup: this.featureSwitchIsDebugging.data
|
||||
? (tags, layer) => new FeatureInfoBox(tags, layer, self)
|
||||
: undefined,
|
||||
state: this,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +1,43 @@
|
|||
/**
|
||||
* The part of the global state which initializes the feature switches, based on default values and on the layoutToUse
|
||||
*/
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {QueryParameters} from "../Web/QueryParameters";
|
||||
import Constants from "../../Models/Constants";
|
||||
import {Utils} from "../../Utils";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export default class FeatureSwitchState {
|
||||
|
||||
/**
|
||||
* The layout that is being used in this run
|
||||
*/
|
||||
public readonly layoutToUse: LayoutConfig;
|
||||
public readonly layoutToUse: LayoutConfig
|
||||
|
||||
public readonly featureSwitchUserbadge: UIEventSource<boolean>;
|
||||
public readonly featureSwitchSearch: UIEventSource<boolean>;
|
||||
public readonly featureSwitchBackgroundSelection: UIEventSource<boolean>;
|
||||
public readonly featureSwitchAddNew: UIEventSource<boolean>;
|
||||
public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>;
|
||||
public readonly featureSwitchExtraLinkEnabled: UIEventSource<boolean>;
|
||||
public readonly featureSwitchMoreQuests: UIEventSource<boolean>;
|
||||
public readonly featureSwitchShareScreen: UIEventSource<boolean>;
|
||||
public readonly featureSwitchGeolocation: UIEventSource<boolean>;
|
||||
public readonly featureSwitchIsTesting: UIEventSource<boolean>;
|
||||
public readonly featureSwitchIsDebugging: UIEventSource<boolean>;
|
||||
public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>;
|
||||
public readonly featureSwitchApiURL: UIEventSource<string>;
|
||||
public readonly featureSwitchFilter: UIEventSource<boolean>;
|
||||
public readonly featureSwitchEnableExport: UIEventSource<boolean>;
|
||||
public readonly featureSwitchFakeUser: UIEventSource<boolean>;
|
||||
public readonly featureSwitchExportAsPdf: UIEventSource<boolean>;
|
||||
public readonly overpassUrl: UIEventSource<string[]>;
|
||||
public readonly overpassTimeout: UIEventSource<number>;
|
||||
public readonly overpassMaxZoom: UIEventSource<number>;
|
||||
public readonly osmApiTileSize: UIEventSource<number>;
|
||||
public readonly backgroundLayerId: UIEventSource<string>;
|
||||
public readonly featureSwitchUserbadge: UIEventSource<boolean>
|
||||
public readonly featureSwitchSearch: UIEventSource<boolean>
|
||||
public readonly featureSwitchBackgroundSelection: UIEventSource<boolean>
|
||||
public readonly featureSwitchAddNew: UIEventSource<boolean>
|
||||
public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>
|
||||
public readonly featureSwitchExtraLinkEnabled: UIEventSource<boolean>
|
||||
public readonly featureSwitchMoreQuests: UIEventSource<boolean>
|
||||
public readonly featureSwitchShareScreen: UIEventSource<boolean>
|
||||
public readonly featureSwitchGeolocation: UIEventSource<boolean>
|
||||
public readonly featureSwitchIsTesting: UIEventSource<boolean>
|
||||
public readonly featureSwitchIsDebugging: UIEventSource<boolean>
|
||||
public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>
|
||||
public readonly featureSwitchApiURL: UIEventSource<string>
|
||||
public readonly featureSwitchFilter: UIEventSource<boolean>
|
||||
public readonly featureSwitchEnableExport: UIEventSource<boolean>
|
||||
public readonly featureSwitchFakeUser: UIEventSource<boolean>
|
||||
public readonly featureSwitchExportAsPdf: UIEventSource<boolean>
|
||||
public readonly overpassUrl: UIEventSource<string[]>
|
||||
public readonly overpassTimeout: UIEventSource<number>
|
||||
public readonly overpassMaxZoom: UIEventSource<number>
|
||||
public readonly osmApiTileSize: UIEventSource<number>
|
||||
public readonly backgroundLayerId: UIEventSource<string>
|
||||
|
||||
public constructor(layoutToUse: LayoutConfig) {
|
||||
this.layoutToUse = layoutToUse;
|
||||
|
||||
this.layoutToUse = layoutToUse
|
||||
|
||||
// Helper function to initialize feature switches
|
||||
function featSw(
|
||||
|
@ -47,104 +45,104 @@ export default class FeatureSwitchState {
|
|||
deflt: (layout: LayoutConfig) => boolean,
|
||||
documentation: string
|
||||
): UIEventSource<boolean> {
|
||||
|
||||
const defaultValue = deflt(layoutToUse);
|
||||
const defaultValue = deflt(layoutToUse)
|
||||
const queryParam = QueryParameters.GetQueryParameter(
|
||||
key,
|
||||
"" + defaultValue,
|
||||
documentation
|
||||
);
|
||||
|
||||
// It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
|
||||
return queryParam.sync((str) =>
|
||||
str === undefined ? defaultValue : str !== "false", [],
|
||||
b => b == defaultValue ? undefined : (""+b)
|
||||
)
|
||||
|
||||
// It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
|
||||
return queryParam.sync(
|
||||
(str) => (str === undefined ? defaultValue : str !== "false"),
|
||||
[],
|
||||
(b) => (b == defaultValue ? undefined : "" + b)
|
||||
)
|
||||
}
|
||||
|
||||
this.featureSwitchUserbadge = featSw(
|
||||
"fs-userbadge",
|
||||
(layoutToUse) => layoutToUse?.enableUserBadge ?? true,
|
||||
"Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode."
|
||||
);
|
||||
)
|
||||
this.featureSwitchSearch = featSw(
|
||||
"fs-search",
|
||||
(layoutToUse) => layoutToUse?.enableSearch ?? true,
|
||||
"Disables/Enables the search bar"
|
||||
);
|
||||
)
|
||||
this.featureSwitchBackgroundSelection = featSw(
|
||||
"fs-background",
|
||||
(layoutToUse) => layoutToUse?.enableBackgroundLayerSelection ?? true,
|
||||
"Disables/Enables the background layer control"
|
||||
);
|
||||
)
|
||||
|
||||
this.featureSwitchFilter = featSw(
|
||||
"fs-filter",
|
||||
(layoutToUse) => layoutToUse?.enableLayers ?? true,
|
||||
"Disables/Enables the filter view"
|
||||
);
|
||||
)
|
||||
this.featureSwitchAddNew = featSw(
|
||||
"fs-add-new",
|
||||
(layoutToUse) => layoutToUse?.enableAddNewPoints ?? true,
|
||||
"Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)"
|
||||
);
|
||||
)
|
||||
this.featureSwitchWelcomeMessage = featSw(
|
||||
"fs-welcome-message",
|
||||
() => true,
|
||||
"Disables/enables the help menu or welcome message"
|
||||
);
|
||||
)
|
||||
this.featureSwitchExtraLinkEnabled = featSw(
|
||||
"fs-iframe-popout",
|
||||
_ => true,
|
||||
(_) => true,
|
||||
"Disables/Enables the extraLink button. By default, if in iframe mode and the welcome message is hidden, a popout button to the full mapcomplete instance is shown instead (unless disabled with this switch or another extraLink button is enabled)"
|
||||
);
|
||||
)
|
||||
this.featureSwitchMoreQuests = featSw(
|
||||
"fs-more-quests",
|
||||
(layoutToUse) => layoutToUse?.enableMoreQuests ?? true,
|
||||
"Disables/Enables the 'More Quests'-tab in the welcome message"
|
||||
);
|
||||
)
|
||||
this.featureSwitchShareScreen = featSw(
|
||||
"fs-share-screen",
|
||||
(layoutToUse) => layoutToUse?.enableShareScreen ?? true,
|
||||
"Disables/Enables the 'Share-screen'-tab in the welcome message"
|
||||
);
|
||||
)
|
||||
this.featureSwitchGeolocation = featSw(
|
||||
"fs-geolocation",
|
||||
(layoutToUse) => layoutToUse?.enableGeolocation ?? true,
|
||||
"Disables/Enables the geolocation button"
|
||||
);
|
||||
)
|
||||
this.featureSwitchShowAllQuestions = featSw(
|
||||
"fs-all-questions",
|
||||
(layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false,
|
||||
"Always show all questions"
|
||||
);
|
||||
)
|
||||
|
||||
this.featureSwitchEnableExport = featSw(
|
||||
"fs-export",
|
||||
(layoutToUse) => layoutToUse?.enableExportButton ?? false,
|
||||
"Enable the export as GeoJSON and CSV button"
|
||||
);
|
||||
)
|
||||
this.featureSwitchExportAsPdf = featSw(
|
||||
"fs-pdf",
|
||||
(layoutToUse) => layoutToUse?.enablePdfDownload ?? false,
|
||||
"Enable the PDF download button"
|
||||
);
|
||||
)
|
||||
|
||||
this.featureSwitchApiURL = QueryParameters.GetQueryParameter(
|
||||
"backend",
|
||||
"osm",
|
||||
"The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'"
|
||||
);
|
||||
)
|
||||
|
||||
|
||||
let testingDefaultValue = false;
|
||||
if (this.featureSwitchApiURL.data !== "osm-test" && !Utils.runningFromConsole &&
|
||||
(location.hostname === "localhost" || location.hostname === "127.0.0.1")) {
|
||||
let testingDefaultValue = false
|
||||
if (
|
||||
this.featureSwitchApiURL.data !== "osm-test" &&
|
||||
!Utils.runningFromConsole &&
|
||||
(location.hostname === "localhost" || location.hostname === "127.0.0.1")
|
||||
) {
|
||||
testingDefaultValue = true
|
||||
}
|
||||
|
||||
|
||||
this.featureSwitchIsTesting = QueryParameters.GetBooleanQueryParameter(
|
||||
"test",
|
||||
testingDefaultValue,
|
||||
|
@ -157,31 +155,47 @@ export default class FeatureSwitchState {
|
|||
"If true, shows some extra debugging help such as all the available tags on every object"
|
||||
)
|
||||
|
||||
this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter("fake-user", false,
|
||||
"If true, 'dryrun' mode is activated and a fake user account is loaded")
|
||||
this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter(
|
||||
"fake-user",
|
||||
false,
|
||||
"If true, 'dryrun' mode is activated and a fake user account is loaded"
|
||||
)
|
||||
|
||||
|
||||
this.overpassUrl = QueryParameters.GetQueryParameter("overpassUrl",
|
||||
this.overpassUrl = QueryParameters.GetQueryParameter(
|
||||
"overpassUrl",
|
||||
(layoutToUse?.overpassUrl ?? Constants.defaultOverpassUrls).join(","),
|
||||
"Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter"
|
||||
).sync(param => param.split(","), [], urls => urls.join(","))
|
||||
).sync(
|
||||
(param) => param.split(","),
|
||||
[],
|
||||
(urls) => urls.join(",")
|
||||
)
|
||||
|
||||
this.overpassTimeout = UIEventSource.asFloat(QueryParameters.GetQueryParameter("overpassTimeout",
|
||||
"" + layoutToUse?.overpassTimeout,
|
||||
"Set a different timeout (in seconds) for queries in overpass"))
|
||||
this.overpassTimeout = UIEventSource.asFloat(
|
||||
QueryParameters.GetQueryParameter(
|
||||
"overpassTimeout",
|
||||
"" + layoutToUse?.overpassTimeout,
|
||||
"Set a different timeout (in seconds) for queries in overpass"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
this.overpassMaxZoom =
|
||||
UIEventSource.asFloat(QueryParameters.GetQueryParameter("overpassMaxZoom",
|
||||
this.overpassMaxZoom = UIEventSource.asFloat(
|
||||
QueryParameters.GetQueryParameter(
|
||||
"overpassMaxZoom",
|
||||
"" + layoutToUse?.overpassMaxZoom,
|
||||
" point to switch between OSM-api and overpass"))
|
||||
" point to switch between OSM-api and overpass"
|
||||
)
|
||||
)
|
||||
|
||||
this.osmApiTileSize =
|
||||
UIEventSource.asFloat(QueryParameters.GetQueryParameter("osmApiTileSize",
|
||||
this.osmApiTileSize = UIEventSource.asFloat(
|
||||
QueryParameters.GetQueryParameter(
|
||||
"osmApiTileSize",
|
||||
"" + layoutToUse?.osmApiTileSize,
|
||||
"Tilesize when the OSM-API is used to fetch data within a BBOX"))
|
||||
"Tilesize when the OSM-API is used to fetch data within a BBOX"
|
||||
)
|
||||
)
|
||||
|
||||
this.featureSwitchUserbadge.addCallbackAndRun(userbadge => {
|
||||
this.featureSwitchUserbadge.addCallbackAndRun((userbadge) => {
|
||||
if (!userbadge) {
|
||||
this.featureSwitchAddNew.setData(false)
|
||||
}
|
||||
|
@ -191,9 +205,6 @@ export default class FeatureSwitchState {
|
|||
"background",
|
||||
layoutToUse?.defaultBackgroundId ?? "osm",
|
||||
"The id of the background layer to start with"
|
||||
);
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,33 @@
|
|||
import UserRelatedState from "./UserRelatedState";
|
||||
import {Store, Stores, UIEventSource} from "../UIEventSource";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import AvailableBaseLayers from "../Actors/AvailableBaseLayers";
|
||||
import Attribution from "../../UI/BigComponents/Attribution";
|
||||
import Minimap, {MinimapObj} from "../../UI/Base/Minimap";
|
||||
import {Tiles} from "../../Models/TileRange";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
|
||||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
|
||||
import {QueryParameters} from "../Web/QueryParameters";
|
||||
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource";
|
||||
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource";
|
||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||
import {GeoOperations} from "../GeoOperations";
|
||||
import TitleHandler from "../Actors/TitleHandler";
|
||||
import {BBox} from "../BBox";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {TiledStaticFeatureSource} from "../FeatureSource/Sources/StaticFeatureSource";
|
||||
import {Translation, TypedTranslation} from "../../UI/i18n/Translation";
|
||||
import {Tag} from "../Tags/Tag";
|
||||
import {OsmConnection} from "../Osm/OsmConnection";
|
||||
|
||||
import UserRelatedState from "./UserRelatedState"
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import AvailableBaseLayers from "../Actors/AvailableBaseLayers"
|
||||
import Attribution from "../../UI/BigComponents/Attribution"
|
||||
import Minimap, { MinimapObj } from "../../UI/Base/Minimap"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
|
||||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource"
|
||||
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import TitleHandler from "../Actors/TitleHandler"
|
||||
import { BBox } from "../BBox"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { TiledStaticFeatureSource } from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import { Translation, TypedTranslation } from "../../UI/i18n/Translation"
|
||||
import { Tag } from "../Tags/Tag"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
|
||||
export interface GlobalFilter {
|
||||
filter: FilterState,
|
||||
id: string,
|
||||
filter: FilterState
|
||||
id: string
|
||||
onNewPoint: {
|
||||
safetyCheck: Translation,
|
||||
safetyCheck: Translation
|
||||
confirmAddNew: TypedTranslation<{ preset: Translation }>
|
||||
tags: Tag[]
|
||||
}
|
||||
|
@ -38,60 +37,64 @@ export interface GlobalFilter {
|
|||
* Contains all the leaflet-map related state
|
||||
*/
|
||||
export default class MapState extends UserRelatedState {
|
||||
|
||||
/**
|
||||
The leaflet instance of the big basemap
|
||||
*/
|
||||
public leafletMap = new UIEventSource<any /*L.Map*/>(undefined, "leafletmap");
|
||||
public leafletMap = new UIEventSource<any /*L.Map*/>(undefined, "leafletmap")
|
||||
/**
|
||||
* A list of currently available background layers
|
||||
*/
|
||||
public availableBackgroundLayers: Store<BaseLayer[]>;
|
||||
public availableBackgroundLayers: Store<BaseLayer[]>
|
||||
|
||||
/**
|
||||
* The current background layer
|
||||
*/
|
||||
public backgroundLayer: UIEventSource<BaseLayer>;
|
||||
public backgroundLayer: UIEventSource<BaseLayer>
|
||||
/**
|
||||
* Last location where a click was registered
|
||||
*/
|
||||
public readonly LastClickLocation: UIEventSource<{
|
||||
lat: number;
|
||||
lon: number;
|
||||
}> = new UIEventSource<{ lat: number; lon: number }>(undefined);
|
||||
lat: number
|
||||
lon: number
|
||||
}> = new UIEventSource<{ lat: number; lon: number }>(undefined)
|
||||
|
||||
/**
|
||||
* The bounds of the current map view
|
||||
*/
|
||||
public currentView: FeatureSourceForLayer & Tiled;
|
||||
public currentView: FeatureSourceForLayer & Tiled
|
||||
/**
|
||||
* The location as delivered by the GPS
|
||||
*/
|
||||
public currentUserLocation: SimpleFeatureSource;
|
||||
public currentUserLocation: SimpleFeatureSource
|
||||
|
||||
/**
|
||||
* All previously visited points
|
||||
*/
|
||||
public historicalUserLocations: SimpleFeatureSource;
|
||||
public historicalUserLocations: SimpleFeatureSource
|
||||
/**
|
||||
* The number of seconds that the GPS-locations are stored in memory.
|
||||
* Time in seconds
|
||||
*/
|
||||
public gpsLocationHistoryRetentionTime = new UIEventSource(7 * 24 * 60 * 60, "gps_location_retention")
|
||||
public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled;
|
||||
public gpsLocationHistoryRetentionTime = new UIEventSource(
|
||||
7 * 24 * 60 * 60,
|
||||
"gps_location_retention"
|
||||
)
|
||||
public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled
|
||||
|
||||
/**
|
||||
* A feature source containing the current home location of the user
|
||||
*/
|
||||
public homeLocation: FeatureSourceForLayer & Tiled
|
||||
|
||||
public readonly mainMapObject: BaseUIElement & MinimapObj;
|
||||
|
||||
public readonly mainMapObject: BaseUIElement & MinimapObj
|
||||
|
||||
/**
|
||||
* Which layers are enabled in the current theme and what filters are applied onto them
|
||||
*/
|
||||
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers");
|
||||
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>(
|
||||
[],
|
||||
"filteredLayers"
|
||||
)
|
||||
|
||||
/**
|
||||
* Filters which apply onto all layers
|
||||
|
@ -101,31 +104,30 @@ export default class MapState extends UserRelatedState {
|
|||
/**
|
||||
* Which overlays are shown
|
||||
*/
|
||||
public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]
|
||||
|
||||
public overlayToggles: { config: TilesourceConfig; isDisplayed: UIEventSource<boolean> }[]
|
||||
|
||||
constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) {
|
||||
super(layoutToUse, options);
|
||||
super(layoutToUse, options)
|
||||
|
||||
this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl);
|
||||
this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl)
|
||||
|
||||
let defaultLayer = AvailableBaseLayers.osmCarto
|
||||
const available = this.availableBackgroundLayers.data;
|
||||
const available = this.availableBackgroundLayers.data
|
||||
for (const layer of available) {
|
||||
if (this.backgroundLayerId.data === layer.id) {
|
||||
defaultLayer = layer;
|
||||
defaultLayer = layer
|
||||
}
|
||||
}
|
||||
const self = this
|
||||
this.backgroundLayer = new UIEventSource<BaseLayer>(defaultLayer)
|
||||
this.backgroundLayer.addCallbackAndRunD(layer => self.backgroundLayerId.setData(layer.id))
|
||||
this.backgroundLayer.addCallbackAndRunD((layer) => self.backgroundLayerId.setData(layer.id))
|
||||
|
||||
const attr = new Attribution(
|
||||
this.locationControl,
|
||||
this.osmConnection.userDetails,
|
||||
this.layoutToUse,
|
||||
this.currentBounds
|
||||
);
|
||||
)
|
||||
|
||||
// Will write into this.leafletMap
|
||||
this.mainMapObject = Minimap.createMiniMap({
|
||||
|
@ -134,18 +136,23 @@ export default class MapState extends UserRelatedState {
|
|||
leafletMap: this.leafletMap,
|
||||
bounds: this.currentBounds,
|
||||
attribution: attr,
|
||||
lastClickLocation: this.LastClickLocation
|
||||
lastClickLocation: this.LastClickLocation,
|
||||
})
|
||||
|
||||
|
||||
this.overlayToggles = this.layoutToUse?.tileLayerSources
|
||||
?.filter(c => c.name !== undefined)
|
||||
?.map(c => ({
|
||||
config: c,
|
||||
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.overlayToggles =
|
||||
this.layoutToUse?.tileLayerSources
|
||||
?.filter((c) => c.name !== undefined)
|
||||
?.map((c) => ({
|
||||
config: c,
|
||||
isDisplayed: QueryParameters.GetBooleanQueryParameter(
|
||||
"overlay-" + c.id,
|
||||
c.defaultState,
|
||||
"Wether or not the overlay " + c.id + " is shown"
|
||||
),
|
||||
})) ?? []
|
||||
this.filteredLayers = new UIEventSource<FilteredLayer[]>(
|
||||
MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection)
|
||||
)
|
||||
|
||||
this.lockBounds()
|
||||
this.AddAllOverlaysToMap(this.leafletMap)
|
||||
|
@ -155,7 +162,7 @@ export default class MapState extends UserRelatedState {
|
|||
this.initUserLocationTrail()
|
||||
this.initCurrentView()
|
||||
|
||||
new TitleHandler(this);
|
||||
new TitleHandler(this)
|
||||
}
|
||||
|
||||
public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) {
|
||||
|
@ -171,15 +178,14 @@ export default class MapState extends UserRelatedState {
|
|||
}
|
||||
new ShowOverlayLayer(tileLayerSource, leafletMap)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private lockBounds() {
|
||||
const layout = this.layoutToUse;
|
||||
const layout = this.layoutToUse
|
||||
if (!layout?.lockLocation) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
console.warn("Locking the bounds to ", layout.lockLocation);
|
||||
console.warn("Locking the bounds to ", layout.lockLocation)
|
||||
this.mainMapObject.installBounds(
|
||||
new BBox(layout.lockLocation),
|
||||
this.featureSwitchIsTesting.data
|
||||
|
@ -187,69 +193,82 @@ export default class MapState extends UserRelatedState {
|
|||
}
|
||||
|
||||
private initCurrentView() {
|
||||
let currentViewLayer: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "current_view")[0]
|
||||
let currentViewLayer: FilteredLayer = this.filteredLayers.data.filter(
|
||||
(l) => l.layerDef.id === "current_view"
|
||||
)[0]
|
||||
|
||||
if (currentViewLayer === undefined) {
|
||||
// This layer is not needed by the theme and thus unloaded
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let i = 0
|
||||
const self = this;
|
||||
const features: Store<{ feature: any, freshness: Date }[]> = this.currentBounds.map(bounds => {
|
||||
if (bounds === undefined) {
|
||||
return []
|
||||
}
|
||||
i++
|
||||
const feature = {
|
||||
freshness: new Date(),
|
||||
feature: {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: "current_view-" + i,
|
||||
"current_view": "yes",
|
||||
"zoom": "" + self.locationControl.data.zoom
|
||||
},
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[
|
||||
[bounds.maxLon, bounds.maxLat],
|
||||
[bounds.minLon, bounds.maxLat],
|
||||
[bounds.minLon, bounds.minLat],
|
||||
[bounds.maxLon, bounds.minLat],
|
||||
[bounds.maxLon, bounds.maxLat],
|
||||
]]
|
||||
}
|
||||
const self = this
|
||||
const features: Store<{ feature: any; freshness: Date }[]> = this.currentBounds.map(
|
||||
(bounds) => {
|
||||
if (bounds === undefined) {
|
||||
return []
|
||||
}
|
||||
i++
|
||||
const feature = {
|
||||
freshness: new Date(),
|
||||
feature: {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: "current_view-" + i,
|
||||
current_view: "yes",
|
||||
zoom: "" + self.locationControl.data.zoom,
|
||||
},
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [
|
||||
[
|
||||
[bounds.maxLon, bounds.maxLat],
|
||||
[bounds.minLon, bounds.maxLat],
|
||||
[bounds.minLon, bounds.minLat],
|
||||
[bounds.maxLon, bounds.minLat],
|
||||
[bounds.maxLon, bounds.maxLat],
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
return [feature]
|
||||
}
|
||||
return [feature]
|
||||
})
|
||||
)
|
||||
|
||||
this.currentView = new TiledStaticFeatureSource(features, currentViewLayer);
|
||||
this.currentView = new TiledStaticFeatureSource(features, currentViewLayer)
|
||||
}
|
||||
|
||||
private initGpsLocation() {
|
||||
// Initialize the gps layer data. This is emtpy for now, the actual writing happens in the Geolocationhandler
|
||||
let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_location")[0]
|
||||
let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(
|
||||
(l) => l.layerDef.id === "gps_location"
|
||||
)[0]
|
||||
if (gpsLayerDef === undefined) {
|
||||
return
|
||||
}
|
||||
this.currentUserLocation = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0));
|
||||
this.currentUserLocation = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0))
|
||||
}
|
||||
|
||||
private initUserLocationTrail() {
|
||||
const features = LocalStorageSource.GetParsed<{ feature: any, freshness: Date }[]>("gps_location_history", [])
|
||||
const features = LocalStorageSource.GetParsed<{ feature: any; freshness: Date }[]>(
|
||||
"gps_location_history",
|
||||
[]
|
||||
)
|
||||
const now = new Date().getTime()
|
||||
features.data = features.data
|
||||
.map(ff => ({feature: ff.feature, freshness: new Date(ff.freshness)}))
|
||||
.filter(ff => (now - ff.freshness.getTime()) < 1000 * this.gpsLocationHistoryRetentionTime.data)
|
||||
.map((ff) => ({ feature: ff.feature, freshness: new Date(ff.freshness) }))
|
||||
.filter(
|
||||
(ff) =>
|
||||
now - ff.freshness.getTime() < 1000 * this.gpsLocationHistoryRetentionTime.data
|
||||
)
|
||||
features.ping()
|
||||
const self = this;
|
||||
const self = this
|
||||
let i = 0
|
||||
this.currentUserLocation?.features?.addCallbackAndRunD(([location]) => {
|
||||
if (location === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const previousLocation = features.data[features.data.length - 1]
|
||||
|
@ -261,30 +280,37 @@ export default class MapState extends UserRelatedState {
|
|||
let timeDiff = Number.MAX_VALUE // in seconds
|
||||
const olderLocation = features.data[features.data.length - 2]
|
||||
if (olderLocation !== undefined) {
|
||||
timeDiff = (new Date(previousLocation.freshness).getTime() - new Date(olderLocation.freshness).getTime()) / 1000
|
||||
timeDiff =
|
||||
(new Date(previousLocation.freshness).getTime() -
|
||||
new Date(olderLocation.freshness).getTime()) /
|
||||
1000
|
||||
}
|
||||
if (d < 20 && timeDiff < 60) {
|
||||
// Do not append changes less then 20m - it's probably noise anyway
|
||||
return;
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const feature = JSON.parse(JSON.stringify(location.feature))
|
||||
feature.properties.id = "gps/" + features.data.length
|
||||
i++
|
||||
features.data.push({feature, freshness: new Date()})
|
||||
features.data.push({ feature, freshness: new Date() })
|
||||
features.ping()
|
||||
})
|
||||
|
||||
|
||||
let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_location_history")[0]
|
||||
let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(
|
||||
(l) => l.layerDef.id === "gps_location_history"
|
||||
)[0]
|
||||
if (gpsLayerDef !== undefined) {
|
||||
this.historicalUserLocations = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0), features);
|
||||
this.historicalUserLocations = new SimpleFeatureSource(
|
||||
gpsLayerDef,
|
||||
Tiles.tile_index(0, 0, 0),
|
||||
features
|
||||
)
|
||||
this.changes.setHistoricalUserLocations(this.historicalUserLocations)
|
||||
}
|
||||
|
||||
|
||||
const asLine = features.map(allPoints => {
|
||||
const asLine = features.map((allPoints) => {
|
||||
if (allPoints === undefined || allPoints.length < 2) {
|
||||
return []
|
||||
}
|
||||
|
@ -292,136 +318,184 @@ export default class MapState extends UserRelatedState {
|
|||
const feature = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"id": "location_track",
|
||||
id: "location_track",
|
||||
"_date:now": new Date().toISOString(),
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: allPoints.map(ff => ff.feature.geometry.coordinates)
|
||||
}
|
||||
coordinates: allPoints.map((ff) => ff.feature.geometry.coordinates),
|
||||
},
|
||||
}
|
||||
|
||||
self.allElements.ContainingFeatures.set(feature.properties.id, feature)
|
||||
|
||||
return [{
|
||||
feature,
|
||||
freshness: new Date()
|
||||
}]
|
||||
return [
|
||||
{
|
||||
feature,
|
||||
freshness: new Date(),
|
||||
},
|
||||
]
|
||||
})
|
||||
let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_track")[0]
|
||||
let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter(
|
||||
(l) => l.layerDef.id === "gps_track"
|
||||
)[0]
|
||||
if (gpsLineLayerDef !== undefined) {
|
||||
this.historicalUserLocationsTrack = new TiledStaticFeatureSource(asLine, gpsLineLayerDef);
|
||||
this.historicalUserLocationsTrack = new TiledStaticFeatureSource(
|
||||
asLine,
|
||||
gpsLineLayerDef
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private initHomeLocation() {
|
||||
const empty = []
|
||||
const feature = Stores.ListStabilized(this.osmConnection.userDetails.map(userDetails => {
|
||||
|
||||
if (userDetails === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const home = userDetails.home;
|
||||
if (home === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return [home.lon, home.lat]
|
||||
})).map(homeLonLat => {
|
||||
const feature = Stores.ListStabilized(
|
||||
this.osmConnection.userDetails.map((userDetails) => {
|
||||
if (userDetails === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const home = userDetails.home
|
||||
if (home === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return [home.lon, home.lat]
|
||||
})
|
||||
).map((homeLonLat) => {
|
||||
if (homeLonLat === undefined) {
|
||||
return empty
|
||||
}
|
||||
return [{
|
||||
feature: {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"id": "home",
|
||||
"user:home": "yes",
|
||||
"_lon": homeLonLat[0],
|
||||
"_lat": homeLonLat[1]
|
||||
return [
|
||||
{
|
||||
feature: {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: "home",
|
||||
"user:home": "yes",
|
||||
_lon: homeLonLat[0],
|
||||
_lat: homeLonLat[1],
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: homeLonLat,
|
||||
},
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"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) {
|
||||
this.homeLocation = new TiledStaticFeatureSource(feature, flayer)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static getPref(osmConnection: OsmConnection, key: string, layer: LayerConfig): UIEventSource<boolean> {
|
||||
return osmConnection
|
||||
.GetPreference(key, layer.shownByDefault + "")
|
||||
.sync(v => {
|
||||
private static getPref(
|
||||
osmConnection: OsmConnection,
|
||||
key: string,
|
||||
layer: LayerConfig
|
||||
): UIEventSource<boolean> {
|
||||
return osmConnection.GetPreference(key, layer.shownByDefault + "").sync(
|
||||
(v) => {
|
||||
if (v === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return v === "true";
|
||||
}, [], b => {
|
||||
return v === "true"
|
||||
},
|
||||
[],
|
||||
(b) => {
|
||||
if (b === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return "" + b;
|
||||
})
|
||||
return "" + b
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public static InitializeFilteredLayers(layoutToUse: {layers: LayerConfig[], id: string}, osmConnection: OsmConnection): FilteredLayer[] {
|
||||
public static InitializeFilteredLayers(
|
||||
layoutToUse: { layers: LayerConfig[]; id: string },
|
||||
osmConnection: OsmConnection
|
||||
): FilteredLayer[] {
|
||||
if (layoutToUse === undefined) {
|
||||
return []
|
||||
}
|
||||
const flayers: FilteredLayer[] = [];
|
||||
const flayers: FilteredLayer[] = []
|
||||
for (const layer of layoutToUse.layers) {
|
||||
let isDisplayed: UIEventSource<boolean>
|
||||
if (layer.syncSelection === "local") {
|
||||
isDisplayed = LocalStorageSource.GetParsed(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer.shownByDefault)
|
||||
isDisplayed = LocalStorageSource.GetParsed(
|
||||
layoutToUse.id + "-layer-" + layer.id + "-enabled",
|
||||
layer.shownByDefault
|
||||
)
|
||||
} else if (layer.syncSelection === "theme-only") {
|
||||
isDisplayed = MapState.getPref(osmConnection, layoutToUse.id + "-layer-" + layer.id + "-enabled", layer)
|
||||
isDisplayed = MapState.getPref(
|
||||
osmConnection,
|
||||
layoutToUse.id + "-layer-" + layer.id + "-enabled",
|
||||
layer
|
||||
)
|
||||
} else if (layer.syncSelection === "global") {
|
||||
isDisplayed = MapState.getPref(osmConnection,"layer-" + layer.id + "-enabled", layer)
|
||||
isDisplayed = MapState.getPref(
|
||||
osmConnection,
|
||||
"layer-" + layer.id + "-enabled",
|
||||
layer
|
||||
)
|
||||
} else {
|
||||
isDisplayed = QueryParameters.GetBooleanQueryParameter("layer-" + layer.id, layer.shownByDefault, "Wether or not layer " + layer.id + " is shown")
|
||||
isDisplayed = QueryParameters.GetBooleanQueryParameter(
|
||||
"layer-" + layer.id,
|
||||
layer.shownByDefault,
|
||||
"Wether or not layer " + layer.id + " is shown"
|
||||
)
|
||||
}
|
||||
|
||||
const flayer: FilteredLayer = {
|
||||
isDisplayed,
|
||||
layerDef: layer,
|
||||
appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>())
|
||||
};
|
||||
layer.filters.forEach(filterConfig => {
|
||||
appliedFilters: new UIEventSource<Map<string, FilterState>>(
|
||||
new Map<string, FilterState>()
|
||||
),
|
||||
}
|
||||
layer.filters.forEach((filterConfig) => {
|
||||
const stateSrc = filterConfig.initState()
|
||||
|
||||
stateSrc.addCallbackAndRun(state => flayer.appliedFilters.data.set(filterConfig.id, state))
|
||||
flayer.appliedFilters.map(dict => dict.get(filterConfig.id))
|
||||
.addCallback(state => stateSrc.setData(state))
|
||||
stateSrc.addCallbackAndRun((state) =>
|
||||
flayer.appliedFilters.data.set(filterConfig.id, state)
|
||||
)
|
||||
flayer.appliedFilters
|
||||
.map((dict) => dict.get(filterConfig.id))
|
||||
.addCallback((state) => stateSrc.setData(state))
|
||||
})
|
||||
|
||||
flayers.push(flayer);
|
||||
flayers.push(flayer)
|
||||
}
|
||||
|
||||
for (const layer of layoutToUse.layers) {
|
||||
if (layer.filterIsSameAs === undefined) {
|
||||
continue
|
||||
}
|
||||
const toReuse = flayers.find(l => l.layerDef.id === layer.filterIsSameAs)
|
||||
const toReuse = flayers.find((l) => l.layerDef.id === layer.filterIsSameAs)
|
||||
if (toReuse === undefined) {
|
||||
throw "Error in layer " + layer.id + ": it defines that it should be use the filters of " + layer.filterIsSameAs + ", but this layer was not loaded"
|
||||
throw (
|
||||
"Error in layer " +
|
||||
layer.id +
|
||||
": it defines that it should be use the filters of " +
|
||||
layer.filterIsSameAs +
|
||||
", but this layer was not loaded"
|
||||
)
|
||||
}
|
||||
console.warn("Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs)
|
||||
const selfLayer = flayers.findIndex(l => l.layerDef.id === layer.id)
|
||||
console.warn(
|
||||
"Linking filter and isDisplayed-states of " +
|
||||
layer.id +
|
||||
" and " +
|
||||
layer.filterIsSameAs
|
||||
)
|
||||
const selfLayer = flayers.findIndex((l) => l.layerDef.id === layer.id)
|
||||
flayers[selfLayer] = {
|
||||
isDisplayed: toReuse.isDisplayed,
|
||||
layerDef: layer,
|
||||
appliedFilters: toReuse.appliedFilters
|
||||
};
|
||||
appliedFilters: toReuse.appliedFilters,
|
||||
}
|
||||
}
|
||||
|
||||
return flayers;
|
||||
return flayers
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,51 +1,48 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {OsmConnection} from "../Osm/OsmConnection";
|
||||
import {MangroveIdentity} from "../Web/MangroveReviews";
|
||||
import {Store, UIEventSource} from "../UIEventSource";
|
||||
import {QueryParameters} from "../Web/QueryParameters";
|
||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import Locale from "../../UI/i18n/Locale";
|
||||
import ElementsState from "./ElementsState";
|
||||
import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater";
|
||||
import {Changes} from "../Osm/Changes";
|
||||
import ChangeToElementsActor from "../Actors/ChangeToElementsActor";
|
||||
import PendingChangesUploader from "../Actors/PendingChangesUploader";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
import { MangroveIdentity } from "../Web/MangroveReviews"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { Utils } from "../../Utils"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import ElementsState from "./ElementsState"
|
||||
import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater"
|
||||
import { Changes } from "../Osm/Changes"
|
||||
import ChangeToElementsActor from "../Actors/ChangeToElementsActor"
|
||||
import PendingChangesUploader from "../Actors/PendingChangesUploader"
|
||||
import * as translators from "../../assets/translators.json"
|
||||
import {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,
|
||||
* which layers they enabled, ...
|
||||
*/
|
||||
export default class UserRelatedState extends ElementsState {
|
||||
|
||||
|
||||
/**
|
||||
The user credentials
|
||||
*/
|
||||
public osmConnection: OsmConnection;
|
||||
public osmConnection: OsmConnection
|
||||
/**
|
||||
THe change handler
|
||||
*/
|
||||
public changes: Changes;
|
||||
public changes: Changes
|
||||
/**
|
||||
* The key for mangrove
|
||||
*/
|
||||
public mangroveIdentity: MangroveIdentity;
|
||||
public mangroveIdentity: MangroveIdentity
|
||||
|
||||
/**
|
||||
* Maproulette connection
|
||||
*/
|
||||
public maprouletteConnection: Maproulette;
|
||||
public maprouletteConnection: Maproulette
|
||||
|
||||
public readonly isTranslator: Store<boolean>
|
||||
|
||||
public readonly isTranslator : Store<boolean>;
|
||||
|
||||
public readonly installedUserThemes: Store<string[]>
|
||||
|
||||
|
||||
constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) {
|
||||
super(layoutToUse);
|
||||
super(layoutToUse)
|
||||
|
||||
this.osmConnection = new OsmConnection({
|
||||
dryRun: this.featureSwitchIsTesting,
|
||||
|
@ -55,138 +52,147 @@ export default class UserRelatedState extends ElementsState {
|
|||
undefined,
|
||||
"Used to complete the login"
|
||||
),
|
||||
osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data,
|
||||
attemptLogin: options?.attemptLogin
|
||||
osmConfiguration: <"osm" | "osm-test">this.featureSwitchApiURL.data,
|
||||
attemptLogin: options?.attemptLogin,
|
||||
})
|
||||
const translationMode = this.osmConnection.GetPreference("translation-mode").sync(str => str === undefined ? undefined : str === "true", [], b => b === undefined ? undefined : b+"")
|
||||
|
||||
const translationMode = this.osmConnection.GetPreference("translation-mode").sync(
|
||||
(str) => (str === undefined ? undefined : str === "true"),
|
||||
[],
|
||||
(b) => (b === undefined ? undefined : b + "")
|
||||
)
|
||||
|
||||
translationMode.syncWith(Locale.showLinkToWeblate)
|
||||
|
||||
this.isTranslator = this.osmConnection.userDetails.map(ud => {
|
||||
if(!ud.loggedIn){
|
||||
return false;
|
||||
|
||||
this.isTranslator = this.osmConnection.userDetails.map((ud) => {
|
||||
if (!ud.loggedIn) {
|
||||
return false
|
||||
}
|
||||
const name= ud.name.toLowerCase().replace(/\s+/g, '')
|
||||
return translators.contributors.some(c => c.contributor.toLowerCase().replace(/\s+/g, '') === name)
|
||||
const name = ud.name.toLowerCase().replace(/\s+/g, "")
|
||||
return translators.contributors.some(
|
||||
(c) => c.contributor.toLowerCase().replace(/\s+/g, "") === name
|
||||
)
|
||||
})
|
||||
|
||||
this.isTranslator.addCallbackAndRunD(ud => {
|
||||
if(ud){
|
||||
|
||||
this.isTranslator.addCallbackAndRunD((ud) => {
|
||||
if (ud) {
|
||||
Locale.showLinkToWeblate.setData(true)
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
this.changes = new Changes(this, layoutToUse?.isLeftRightSensitive() ?? false)
|
||||
|
||||
|
||||
new ChangeToElementsActor(this.changes, this.allElements)
|
||||
new PendingChangesUploader(this.changes, this.selectedElement);
|
||||
|
||||
new PendingChangesUploader(this.changes, this.selectedElement)
|
||||
|
||||
this.mangroveIdentity = new MangroveIdentity(
|
||||
this.osmConnection.GetLongPreference("identity", "mangrove")
|
||||
);
|
||||
)
|
||||
|
||||
this.maprouletteConnection = new Maproulette();
|
||||
this.maprouletteConnection = new Maproulette()
|
||||
|
||||
if (layoutToUse?.hideFromOverview) {
|
||||
this.osmConnection.isLoggedIn.addCallbackAndRunD(loggedIn => {
|
||||
this.osmConnection.isLoggedIn.addCallbackAndRunD((loggedIn) => {
|
||||
if (loggedIn) {
|
||||
this.osmConnection
|
||||
.GetPreference("hidden-theme-" + layoutToUse?.id + "-enabled")
|
||||
.setData("true");
|
||||
return true;
|
||||
.setData("true")
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (this.layoutToUse !== undefined && !this.layoutToUse.official) {
|
||||
console.log("Marking unofficial theme as visited")
|
||||
this.osmConnection.GetLongPreference("unofficial-theme-" + this.layoutToUse.id)
|
||||
.setData(JSON.stringify({
|
||||
this.osmConnection.GetLongPreference("unofficial-theme-" + this.layoutToUse.id).setData(
|
||||
JSON.stringify({
|
||||
id: this.layoutToUse.id,
|
||||
icon: this.layoutToUse.icon,
|
||||
title: this.layoutToUse.title.translations,
|
||||
shortDescription: this.layoutToUse.shortDescription.translations,
|
||||
definition: this.layoutToUse["definition"]
|
||||
}))
|
||||
definition: this.layoutToUse["definition"],
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
this.InitializeLanguage();
|
||||
this.InitializeLanguage()
|
||||
new SelectedElementTagsUpdater(this)
|
||||
this.installedUserThemes = this.InitInstalledUserThemes();
|
||||
|
||||
this.installedUserThemes = this.InitInstalledUserThemes()
|
||||
}
|
||||
|
||||
private InitializeLanguage() {
|
||||
const layoutToUse = this.layoutToUse;
|
||||
Locale.language.syncWith(this.osmConnection.GetPreference("language"));
|
||||
Locale.language
|
||||
.addCallback((currentLanguage) => {
|
||||
if (layoutToUse === undefined) {
|
||||
return;
|
||||
}
|
||||
if(Locale.showLinkToWeblate.data){
|
||||
return true; // Disable auto switching as we are in translators mode
|
||||
}
|
||||
if (this.layoutToUse.language.indexOf(currentLanguage) < 0) {
|
||||
console.log(
|
||||
"Resetting language to",
|
||||
layoutToUse.language[0],
|
||||
"as",
|
||||
currentLanguage,
|
||||
" is unsupported"
|
||||
);
|
||||
// The current language is not supported -> switch to a supported one
|
||||
Locale.language.setData(layoutToUse.language[0]);
|
||||
}
|
||||
})
|
||||
Locale.language.ping();
|
||||
const layoutToUse = this.layoutToUse
|
||||
Locale.language.syncWith(this.osmConnection.GetPreference("language"))
|
||||
Locale.language.addCallback((currentLanguage) => {
|
||||
if (layoutToUse === undefined) {
|
||||
return
|
||||
}
|
||||
if (Locale.showLinkToWeblate.data) {
|
||||
return true // Disable auto switching as we are in translators mode
|
||||
}
|
||||
if (this.layoutToUse.language.indexOf(currentLanguage) < 0) {
|
||||
console.log(
|
||||
"Resetting language to",
|
||||
layoutToUse.language[0],
|
||||
"as",
|
||||
currentLanguage,
|
||||
" is unsupported"
|
||||
)
|
||||
// The current language is not supported -> switch to a supported one
|
||||
Locale.language.setData(layoutToUse.language[0])
|
||||
}
|
||||
})
|
||||
Locale.language.ping()
|
||||
}
|
||||
|
||||
private InitInstalledUserThemes(): Store<string[]>{
|
||||
const prefix = "mapcomplete-unofficial-theme-";
|
||||
|
||||
private InitInstalledUserThemes(): Store<string[]> {
|
||||
const prefix = "mapcomplete-unofficial-theme-"
|
||||
const postfix = "-combined-length"
|
||||
return this.osmConnection.preferencesHandler.preferences.map(prefs =>
|
||||
return this.osmConnection.preferencesHandler.preferences.map((prefs) =>
|
||||
Object.keys(prefs)
|
||||
.filter(k => k.startsWith(prefix) && k.endsWith(postfix))
|
||||
.map(k => k.substring(prefix.length, k.length - postfix.length))
|
||||
.filter((k) => k.startsWith(prefix) && k.endsWith(postfix))
|
||||
.map((k) => k.substring(prefix.length, k.length - postfix.length))
|
||||
)
|
||||
}
|
||||
|
||||
public GetUnofficialTheme(id: string): {
|
||||
id: string
|
||||
icon: string,
|
||||
title: any,
|
||||
shortDescription: any,
|
||||
definition?: any,
|
||||
isOfficial: boolean
|
||||
} | undefined {
|
||||
|
||||
public GetUnofficialTheme(id: string):
|
||||
| {
|
||||
id: string
|
||||
icon: string
|
||||
title: any
|
||||
shortDescription: any
|
||||
definition?: any
|
||||
isOfficial: boolean
|
||||
}
|
||||
| undefined {
|
||||
console.log("GETTING UNOFFICIAL THEME")
|
||||
const pref = this.osmConnection.GetLongPreference("unofficial-theme-"+id)
|
||||
const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id)
|
||||
const str = pref.data
|
||||
|
||||
|
||||
if (str === undefined || str === "undefined" || str === "") {
|
||||
pref.setData(null)
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const value: {
|
||||
id: string
|
||||
icon: string,
|
||||
title: any,
|
||||
shortDescription: any,
|
||||
definition?: any,
|
||||
icon: string
|
||||
title: any
|
||||
shortDescription: any
|
||||
definition?: any
|
||||
isOfficial: boolean
|
||||
} = JSON.parse(str)
|
||||
value.isOfficial = false
|
||||
return value;
|
||||
return value
|
||||
} catch (e) {
|
||||
console.warn("Removing theme " + id + " as it could not be parsed from the preferences; the content is:", str)
|
||||
console.warn(
|
||||
"Removing theme " +
|
||||
id +
|
||||
" as it could not be parsed from the preferences; the content is:",
|
||||
str
|
||||
)
|
||||
pref.setData(null)
|
||||
return undefined
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import {TagsFilter} from "./TagsFilter";
|
||||
import {Or} from "./Or";
|
||||
import {TagUtils} from "./TagUtils";
|
||||
import {Tag} from "./Tag";
|
||||
import {RegexTag} from "./RegexTag";
|
||||
import { TagsFilter } from "./TagsFilter"
|
||||
import { Or } from "./Or"
|
||||
import { TagUtils } from "./TagUtils"
|
||||
import { Tag } from "./Tag"
|
||||
import { RegexTag } from "./RegexTag"
|
||||
|
||||
export class And extends TagsFilter {
|
||||
|
||||
public and: TagsFilter[]
|
||||
|
||||
constructor(and: TagsFilter[]) {
|
||||
super();
|
||||
super()
|
||||
this.and = and
|
||||
}
|
||||
|
||||
|
@ -21,11 +20,11 @@ export class And extends TagsFilter {
|
|||
}
|
||||
|
||||
private static combine(filter: string, choices: string[]): string[] {
|
||||
const values = [];
|
||||
const values = []
|
||||
for (const or of choices) {
|
||||
values.push(filter + or);
|
||||
values.push(filter + or)
|
||||
}
|
||||
return values;
|
||||
return values
|
||||
}
|
||||
|
||||
normalize() {
|
||||
|
@ -43,11 +42,11 @@ export class And extends TagsFilter {
|
|||
matchesProperties(tags: any): boolean {
|
||||
for (const tagsFilter of this.and) {
|
||||
if (!tagsFilter.matchesProperties(tags)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,36 +55,37 @@ export class And extends TagsFilter {
|
|||
* and.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]" ]
|
||||
*/
|
||||
asOverpass(): string[] {
|
||||
let allChoices: string[] = null;
|
||||
let allChoices: string[] = null
|
||||
for (const andElement of this.and) {
|
||||
const andElementFilter = andElement.asOverpass();
|
||||
const andElementFilter = andElement.asOverpass()
|
||||
if (allChoices === null) {
|
||||
allChoices = andElementFilter;
|
||||
continue;
|
||||
allChoices = andElementFilter
|
||||
continue
|
||||
}
|
||||
|
||||
const newChoices: string[] = [];
|
||||
const newChoices: string[] = []
|
||||
for (const choice of allChoices) {
|
||||
newChoices.push(
|
||||
...And.combine(choice, andElementFilter)
|
||||
)
|
||||
newChoices.push(...And.combine(choice, andElementFilter))
|
||||
}
|
||||
allChoices = newChoices;
|
||||
allChoices = newChoices
|
||||
}
|
||||
return allChoices;
|
||||
return allChoices
|
||||
}
|
||||
|
||||
asHumanString(linkToWiki: boolean, shorten: boolean, properties) {
|
||||
return this.and.map(t => t.asHumanString(linkToWiki, shorten, properties)).filter(x => x !== "").join("&");
|
||||
return this.and
|
||||
.map((t) => t.asHumanString(linkToWiki, shorten, properties))
|
||||
.filter((x) => x !== "")
|
||||
.join("&")
|
||||
}
|
||||
|
||||
isUsableAsAnswer(): boolean {
|
||||
for (const t of this.and) {
|
||||
if (!t.isUsableAsAnswer()) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,45 +107,44 @@ export class And extends TagsFilter {
|
|||
*/
|
||||
shadows(other: TagsFilter): boolean {
|
||||
if (!(other instanceof And)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
for (const selfTag of this.and) {
|
||||
let matchFound = false;
|
||||
let matchFound = false
|
||||
for (const otherTag of other.and) {
|
||||
matchFound = selfTag.shadows(otherTag);
|
||||
matchFound = selfTag.shadows(otherTag)
|
||||
if (matchFound) {
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!matchFound) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for (const otherTag of other.and) {
|
||||
let matchFound = false;
|
||||
let matchFound = false
|
||||
for (const selfTag of this.and) {
|
||||
matchFound = selfTag.shadows(otherTag);
|
||||
matchFound = selfTag.shadows(otherTag)
|
||||
if (matchFound) {
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!matchFound) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
usedKeys(): string[] {
|
||||
return [].concat(...this.and.map(subkeys => subkeys.usedKeys()));
|
||||
return [].concat(...this.and.map((subkeys) => subkeys.usedKeys()))
|
||||
}
|
||||
|
||||
usedTags(): { key: string; value: string }[] {
|
||||
return [].concat(...this.and.map(subkeys => subkeys.usedTags()));
|
||||
return [].concat(...this.and.map((subkeys) => subkeys.usedTags()))
|
||||
}
|
||||
|
||||
asChange(properties: any): { k: string; v: string }[] {
|
||||
|
@ -153,7 +152,7 @@ export class And extends TagsFilter {
|
|||
for (const tagsFilter of this.and) {
|
||||
result.push(...tagsFilter.asChange(properties))
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -187,7 +186,7 @@ export class And extends TagsFilter {
|
|||
continue
|
||||
}
|
||||
if (r === false) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
newAnds.push(r)
|
||||
continue
|
||||
|
@ -203,7 +202,6 @@ export class And extends TagsFilter {
|
|||
continue
|
||||
}
|
||||
if (!value && tag.shadows(knownExpression)) {
|
||||
|
||||
/**
|
||||
* We know that knownExpression is unmet.
|
||||
* if the tag shadows 'knownExpression' (which is the case when control flows gets here),
|
||||
|
@ -228,49 +226,50 @@ export class And extends TagsFilter {
|
|||
if (this.and.length === 0) {
|
||||
return true
|
||||
}
|
||||
const optimizedRaw = this.and.map(t => t.optimize())
|
||||
.filter(t => t !== true /* true is the neutral element in an AND, we drop them*/)
|
||||
if (optimizedRaw.some(t => t === false)) {
|
||||
const optimizedRaw = this.and
|
||||
.map((t) => t.optimize())
|
||||
.filter((t) => t !== true /* true is the neutral element in an AND, we drop them*/)
|
||||
if (optimizedRaw.some((t) => t === false)) {
|
||||
// We have an AND with a contained false: this is always 'false'
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
const optimized = <TagsFilter[]>optimizedRaw;
|
||||
const optimized = <TagsFilter[]>optimizedRaw
|
||||
|
||||
{
|
||||
// Conflicting keys do return false
|
||||
const properties: object = {}
|
||||
const properties: object = {}
|
||||
for (const opt of optimized) {
|
||||
if (opt instanceof Tag) {
|
||||
properties[opt.key] = opt.value
|
||||
}
|
||||
}
|
||||
for (const opt of optimized) {
|
||||
if(opt instanceof Tag ){
|
||||
const k = opt.key
|
||||
const v = properties[k]
|
||||
if(v === undefined){
|
||||
continue
|
||||
}
|
||||
if(v !== opt.value){
|
||||
// detected an internal conflict
|
||||
return false
|
||||
}
|
||||
}
|
||||
if(opt instanceof RegexTag ){
|
||||
const k = opt.key
|
||||
if(typeof k !== "string"){
|
||||
continue
|
||||
}
|
||||
const v = properties[k]
|
||||
if(v === undefined){
|
||||
continue
|
||||
}
|
||||
if(v !== opt.value){
|
||||
// detected an internal conflict
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const opt of optimized) {
|
||||
if (opt instanceof Tag) {
|
||||
const k = opt.key
|
||||
const v = properties[k]
|
||||
if (v === undefined) {
|
||||
continue
|
||||
}
|
||||
if (v !== opt.value) {
|
||||
// detected an internal conflict
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (opt instanceof RegexTag) {
|
||||
const k = opt.key
|
||||
if (typeof k !== "string") {
|
||||
continue
|
||||
}
|
||||
const v = properties[k]
|
||||
if (v === undefined) {
|
||||
continue
|
||||
}
|
||||
if (v !== opt.value) {
|
||||
// detected an internal conflict
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newAnds: TagsFilter[] = []
|
||||
|
@ -287,7 +286,7 @@ export class And extends TagsFilter {
|
|||
}
|
||||
|
||||
{
|
||||
let dirty = false;
|
||||
let dirty = false
|
||||
do {
|
||||
const cleanedContainedOrs: Or[] = []
|
||||
outer: for (let containedOr of containedOrs) {
|
||||
|
@ -310,8 +309,8 @@ export class And extends TagsFilter {
|
|||
}
|
||||
// the 'or' dissolved into a normal tag -> it has to be added to the newAnds
|
||||
newAnds.push(cleaned)
|
||||
dirty = true; // rerun this algo later on
|
||||
continue outer;
|
||||
dirty = true // rerun this algo later on
|
||||
continue outer
|
||||
}
|
||||
cleanedContainedOrs.push(containedOr)
|
||||
}
|
||||
|
@ -319,30 +318,32 @@ export class And extends TagsFilter {
|
|||
} while (dirty)
|
||||
}
|
||||
|
||||
|
||||
containedOrs = containedOrs.filter(ca => {
|
||||
containedOrs = containedOrs.filter((ca) => {
|
||||
const isShadowed = TagUtils.containsEquivalents(newAnds, ca.or)
|
||||
// If 'isShadowed', then at least one part of the 'OR' is matched by the outer and, so this means that this OR isn't needed at all
|
||||
// XY & (XY | AB) === XY
|
||||
return !isShadowed;
|
||||
return !isShadowed
|
||||
})
|
||||
|
||||
// Extract common keys from the OR
|
||||
if (containedOrs.length === 1) {
|
||||
newAnds.push(containedOrs[0])
|
||||
} else if (containedOrs.length > 1) {
|
||||
let commonValues: TagsFilter [] = containedOrs[0].or
|
||||
let commonValues: TagsFilter[] = containedOrs[0].or
|
||||
for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++) {
|
||||
const containedOr = containedOrs[i];
|
||||
commonValues = commonValues.filter(cv => containedOr.or.some(candidate => candidate.shadows(cv)))
|
||||
const containedOr = containedOrs[i]
|
||||
commonValues = commonValues.filter((cv) =>
|
||||
containedOr.or.some((candidate) => candidate.shadows(cv))
|
||||
)
|
||||
}
|
||||
if (commonValues.length === 0) {
|
||||
newAnds.push(...containedOrs)
|
||||
} else {
|
||||
const newOrs: TagsFilter[] = []
|
||||
for (const containedOr of containedOrs) {
|
||||
const elements = containedOr.or
|
||||
.filter(candidate => !commonValues.some(cv => cv.shadows(candidate)))
|
||||
const elements = containedOr.or.filter(
|
||||
(candidate) => !commonValues.some((cv) => cv.shadows(candidate))
|
||||
)
|
||||
newOrs.push(Or.construct(elements))
|
||||
}
|
||||
|
||||
|
@ -371,12 +372,11 @@ export class And extends TagsFilter {
|
|||
}
|
||||
|
||||
isNegative(): boolean {
|
||||
return !this.and.some(t => !t.isNegative());
|
||||
return !this.and.some((t) => !t.isNegative())
|
||||
}
|
||||
|
||||
visit(f: (TagsFilter: any) => void) {
|
||||
f(this)
|
||||
this.and.forEach(sub => sub.visit(f))
|
||||
this.and.forEach((sub) => sub.visit(f))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import {TagsFilter} from "./TagsFilter";
|
||||
import { TagsFilter } from "./TagsFilter"
|
||||
|
||||
export default class ComparingTag implements TagsFilter {
|
||||
private readonly _key: string;
|
||||
private readonly _predicate: (value: string) => boolean;
|
||||
private readonly _representation: string;
|
||||
private readonly _key: string
|
||||
private readonly _predicate: (value: string) => boolean
|
||||
private readonly _representation: string
|
||||
|
||||
constructor(key: string, predicate: (value: string | undefined) => boolean, representation: string = "") {
|
||||
this._key = key;
|
||||
this._predicate = predicate;
|
||||
this._representation = representation;
|
||||
constructor(
|
||||
key: string,
|
||||
predicate: (value: string | undefined) => boolean,
|
||||
representation: string = ""
|
||||
) {
|
||||
this._key = key
|
||||
this._predicate = predicate
|
||||
this._representation = representation
|
||||
}
|
||||
|
||||
asChange(properties: any): { k: string; v: string }[] {
|
||||
|
@ -24,16 +28,16 @@ export default class ComparingTag implements TagsFilter {
|
|||
}
|
||||
|
||||
shadows(other: TagsFilter): boolean {
|
||||
return other === this;
|
||||
return other === this
|
||||
}
|
||||
|
||||
isUsableAsAnswer(): boolean {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the properties match
|
||||
*
|
||||
*
|
||||
* const t = new ComparingTag("key", (x => Number(x) < 42))
|
||||
* t.matchesProperties({key: 42}) // => false
|
||||
* t.matchesProperties({key: 41}) // => true
|
||||
|
@ -41,26 +45,26 @@ export default class ComparingTag implements TagsFilter {
|
|||
* t.matchesProperties({differentKey: 42}) // => false
|
||||
*/
|
||||
matchesProperties(properties: any): boolean {
|
||||
return this._predicate(properties[this._key]);
|
||||
return this._predicate(properties[this._key])
|
||||
}
|
||||
|
||||
usedKeys(): string[] {
|
||||
return [this._key];
|
||||
return [this._key]
|
||||
}
|
||||
|
||||
|
||||
usedTags(): { key: string; value: string }[] {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
|
||||
optimize(): TagsFilter | boolean {
|
||||
return this;
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
isNegative(): boolean {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
visit(f: (TagsFilter) => void) {
|
||||
f(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue