",
+ "condition": "addr:housenumber~*"
+ },
+ "location": [
+ "point",
+ "centroid"
+ ]
+ },
+ {
+ "width": {
+ "render": 1
+ }
+ }
]
},
{
@@ -142,7 +155,7 @@
"osmTags": "identificatie~*",
"maxCacheAge": 0
},
- "minzoom": 19,
+ "minzoom": 18,
"calculatedTags": [
"_overlaps_with_buildings=overlapWith(feat)('osm:buildings').filter(f => f.feat.properties.id.indexOf('-') < 0)",
"_overlaps_with=feat(get)('_overlaps_with_buildings').find(f => f.overlap > 1 /* square meter */ )",
@@ -309,7 +322,7 @@
"osmTags": "identificatie~*",
"maxCacheAge": 0
},
- "minzoom": 19,
+ "minzoom": 18,
"calculatedTags": [
"_closed_osm_addr:=closest(feat)('osm:adresses').properties",
"_bag_obj:addr:housenumber=`${feat.properties.huisnummer}${feat.properties.huisletter}${(feat.properties.toevoeging != '') ? '-' : ''}${feat.properties.toevoeging}`",
@@ -339,4 +352,4 @@
}
],
"hideFromOverview": true
-}
\ No newline at end of file
+}
diff --git a/assets/themes/blind_osm/blind_osm.json b/assets/themes/blind_osm/blind_osm.json
index 9532bd0cf..29acc6835 100644
--- a/assets/themes/blind_osm/blind_osm.json
+++ b/assets/themes/blind_osm/blind_osm.json
@@ -68,7 +68,7 @@
{
"builtin": "kerbs",
"override": {
- "minzoom": 19,
+ "minzoom": 18,
"mapRendering": [
{
"iconBadges": [
@@ -112,4 +112,4 @@
},
"stairs"
]
-}
\ No newline at end of file
+}
diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json
index 90f4dc383..f34d3e69c 100644
--- a/assets/themes/climbing/climbing.json
+++ b/assets/themes/climbing/climbing.json
@@ -468,7 +468,10 @@
"guidepost"
],
"override": {
- "minzoom": 15
+ "minzoom": 15,
+ "mapRendering": [{
+ "iconSize": "30,30"
+ }]
}
}
],
diff --git a/assets/themes/guideposts/guideposts.json b/assets/themes/guideposts/guideposts.json
new file mode 100644
index 000000000..30a857b7b
--- /dev/null
+++ b/assets/themes/guideposts/guideposts.json
@@ -0,0 +1,14 @@
+{
+ "id": "guideposts",
+ "title": {
+ "en": "Guideposts"
+ },
+ "description": {
+ "en": "Guideposts (also known as fingerposts or finger posts) are often found along official hiking, cycling, skiing or horseback riding routes to indicate the directions to different destinations. Additionally, they are often named after a region or place and show the altitude.\n\nThe position of a signpost can be used by a hiker/biker/rider/skier as a confirmation of the current position, especially if they use a printed map without a GPS receiver. "
+ },
+ "icon": "./assets/layers/guidepost/guidepost.svg",
+ "startZoom": 2,
+ "layers": [
+ "guidepost"
+ ]
+}
diff --git a/assets/themes/healthcare/healthcare.json b/assets/themes/healthcare/healthcare.json
index 488a88fe3..bcc2c2d96 100644
--- a/assets/themes/healthcare/healthcare.json
+++ b/assets/themes/healthcare/healthcare.json
@@ -111,8 +111,8 @@
"=presets": [],
"=name": null,
"override": {
- "minzoom": 19
+ "minzoom": 18
}
}
]
-}
\ No newline at end of file
+}
diff --git a/assets/themes/onwheels/onwheels.json b/assets/themes/onwheels/onwheels.json
index 38544e0b7..047ade494 100644
--- a/assets/themes/onwheels/onwheels.json
+++ b/assets/themes/onwheels/onwheels.json
@@ -32,7 +32,7 @@
{
"builtin": "indoors",
"override": {
- "minzoom": 19,
+ "minzoom": 18,
"name": null,
"passAllFeatures": true
}
@@ -43,7 +43,7 @@
"name": null,
"tagRendering": null,
"title": "null",
- "minzoom": 19,
+ "minzoom": 18,
"shownByDefault": false
}
},
@@ -71,7 +71,7 @@
{
"builtin": "entrance",
"override": {
- "minzoom": 19,
+ "minzoom": 18,
"mapRendering": [
{
"icon": "circle:white;./assets/themes/onwheels/entrance.svg"
@@ -131,7 +131,7 @@
{
"builtin": "kerbs",
"override": {
- "minzoom": 19,
+ "minzoom": 18,
"syncSelection": "theme-only",
"mapRendering": [
{
@@ -289,7 +289,7 @@
{
"builtin": "toilet",
"override": {
- "minzoom": 19,
+ "minzoom": 18,
"syncSelection": "theme-only",
"mapRendering": [
{
@@ -349,7 +349,7 @@
{
"builtin": "reception_desk",
"override": {
- "minzoom": 19,
+ "minzoom": 18,
"syncSelection": "theme-only"
}
},
@@ -357,7 +357,7 @@
{
"builtin": "elevator",
"override": {
- "minzoom": 19,
+ "minzoom": 18,
"syncSelection": "theme-only",
"mapRendering": [
{
@@ -524,4 +524,4 @@
]
},
"enableDownload": true
-}
\ No newline at end of file
+}
diff --git a/assets/themes/pets/pets.json b/assets/themes/pets/pets.json
index 650095516..d9eaa8963 100644
--- a/assets/themes/pets/pets.json
+++ b/assets/themes/pets/pets.json
@@ -165,7 +165,7 @@
{
"builtin": "food",
"override": {
- "minzoom": 19,
+ "minzoom": 18,
"filter": null,
"name": null
}
@@ -181,7 +181,7 @@
{
"builtin": "shops",
"override": {
- "minzoom": 19,
+ "minzoom": 18,
"filter": null,
"presets": [
{
@@ -220,4 +220,4 @@
}
],
"credits": "Niels Elgaard Larsen"
-}
\ No newline at end of file
+}
diff --git a/assets/themes/stations/stations.json b/assets/themes/stations/stations.json
index fa1aba257..be71fd48a 100644
--- a/assets/themes/stations/stations.json
+++ b/assets/themes/stations/stations.json
@@ -32,7 +32,7 @@
{
"builtin": "indoors",
"override": {
- "minzoom": 19,
+ "minzoom": 18,
"passAllFeatures": true,
"mapRendering": [
{},
@@ -50,7 +50,7 @@
{
"builtin": "stairs",
"override": {
- "minzoom": 19
+ "minzoom": 18
}
},
{
@@ -114,7 +114,7 @@
]
},
"presets": null,
- "minzoom": 19
+ "minzoom": 18
}
},
{
@@ -127,7 +127,7 @@
]
},
"presets": null,
- "minzoom": 19,
+ "minzoom": 18,
"mapRendering": [
{
"icon": "circle:white;./assets/themes/stations/bicycle_parking.svg"
@@ -145,7 +145,7 @@
]
},
"presets": null,
- "minzoom": 19,
+ "minzoom": 18,
"mapRendering": [
{
"icon": "circle:white;./assets/themes/stations/rental_bicycle.svg"
@@ -163,7 +163,7 @@
]
},
"presets": null,
- "minzoom": 19
+ "minzoom": 18
}
},
{
@@ -179,7 +179,7 @@
]
},
"presets": null,
- "minzoom": 19,
+ "minzoom": 18,
"mapRendering+": [
{
"color": "#00f",
@@ -198,7 +198,7 @@
]
},
"presets": null,
- "minzoom": 19,
+ "minzoom": 18,
"mapRendering+": [
{
"color": "yellow",
@@ -219,13 +219,13 @@
"clock"
],
"override": {
- "minzoom": 19
+ "minzoom": 18
}
},
{
"builtin": "bench",
"override": {
- "minzoom": 19,
+ "minzoom": 18,
"mapRendering": [
{
"icon": "./assets/themes/stations/bench.svg"
@@ -236,7 +236,7 @@
{
"builtin": "drinking_water",
"override": {
- "minzoom": 19,
+ "minzoom": 18,
"mapRendering": [
{
"icon": "circle:white;./assets/themes/stations/drinking_water.svg"
@@ -277,7 +277,7 @@
"zh_Hant": "時刻表"
}
},
- "minzoom": 19,
+ "minzoom": 18,
"source": {
"osmTags": {
"and": [
@@ -391,4 +391,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/assets/themes/street_lighting/street_lighting.json b/assets/themes/street_lighting/street_lighting.json
index 799ac2d4c..0a10f413a 100644
--- a/assets/themes/street_lighting/street_lighting.json
+++ b/assets/themes/street_lighting/street_lighting.json
@@ -220,7 +220,7 @@
]
}
},
- "minzoom": 19,
+ "minzoom": 18,
"title": {
"render": {
"en": "Street",
@@ -351,4 +351,4 @@
}
],
"credits": "Robin van der Linde"
-}
\ No newline at end of file
+}
diff --git a/assets/themes/transit/transit.json b/assets/themes/transit/transit.json
index f2d2af665..3f9852676 100644
--- a/assets/themes/transit/transit.json
+++ b/assets/themes/transit/transit.json
@@ -36,22 +36,19 @@
{
"builtin": "bike_parking",
"override": {
- "minzoom": 19,
- "minzoomVisible": 19
+ "minzoom": 18
}
},
{
"builtin": "parking",
"override": {
- "minzoom": 19,
- "minzoomVisible": 19
+ "minzoom": 18
}
},
{
"builtin": "shelter",
"override": {
- "minzoom": 19,
- "minzoomVisible": 19,
+ "minzoom": 18,
"source": {
"osmTags": {
"and": [
@@ -67,4 +64,4 @@
}
],
"credits": "Robin van der Linde"
-}
\ No newline at end of file
+}
diff --git a/langs/en.json b/langs/en.json
index b91b7df7a..739fbd940 100644
--- a/langs/en.json
+++ b/langs/en.json
@@ -124,7 +124,7 @@
"pleaseLogin": "Please log in to add a new feature",
"presetInfo": "The new POI will have {tags}",
"stillLoading": "The data is still loading. Please wait a bit before you add a new feature.",
- "title": "Add a new feature?",
+ "title": "Add a new feature",
"warnVisibleForEveryone": "Your addition will be visible for everyone",
"wrongType": "This feature is not a node or a way and can not be imported",
"zoomInFurther": "Zoom in further to add a feature.",
diff --git a/langs/nl.json b/langs/nl.json
index 70101f0b2..03f4923e4 100644
--- a/langs/nl.json
+++ b/langs/nl.json
@@ -124,7 +124,7 @@
"pleaseLogin": "Gelieve je aan te melden om een object toe te voegen",
"presetInfo": "Het nieuwe object krijgt de attributen {tags}",
"stillLoading": "De data worden nog geladen. Nog even geduld en dan kan je een object toevoegen.",
- "title": "Nieuw object toevoegen?",
+ "title": "Nieuw object toevoegen",
"warnVisibleForEveryone": "Je toevoeging is voor iedereen zichtbaar",
"wrongType": "Dit object is geen punt of lijn en kan daarom niet geïmporteerd worden",
"zoomInFurther": "Gelieve verder in te zoomen om een object toe te voegen.",
diff --git a/package.json b/package.json
index ef52f608c..4f5b4e4de 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,7 @@
},
"scripts": {
"start": "npm run generate:layeroverview && npm run strt",
- "strt": "vite --host",
+ "strt": "vite --host | sed 's/localhost:/127.0.0.1:/g'",
"strttest": "export NODE_OPTIONS=--max_old_space_size=8364 && parcel serve test.html assets/templates/*.svg assets/templates/fonts/*.ttf",
"watch:css": "tailwindcss -i src/index.css -o public/css/index-tailwind-output.css --watch",
"generate:css": "tailwindcss -i src/index.css -o public/css/index-tailwind-output.css",
diff --git a/scripts/build.sh b/scripts/build.sh
index 6649d61ca..b530e79ea 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -8,8 +8,7 @@ rm -rf dist/*
rm -rf .cache
mkdir dist 2> /dev/null
mkdir dist/assets 2> /dev/null
-mkdir dist/assets/langs 2> /dev/null
-mkdir dist/assets/langs/layers 2> /dev/null
+
export NODE_OPTIONS="--max-old-space-size=8192"
@@ -48,11 +47,12 @@ fi
export NODE_OPTIONS=--max-old-space-size=7000
vite build $SRC_MAPS
-
-
# Copy the layer files, as these might contain assets (e.g. svgs)
cp -r assets/layers/ dist/assets/layers/
cp -r assets/themes/ dist/assets/themes/
cp -r assets/svg/ dist/assets/svg/
-cp -r langs/layers/ dist/assets/langs/layers/
+mkdir dist/assets/langs
+mkdir dist/assets/langs/layers
+cp -r langs/layers/ dist/assets/langs/
+ls dist/assets/langs/layers/
export NODE_OPTIONS=""
diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts
index c938e07dc..e7e8d676f 100644
--- a/scripts/generateLayouts.ts
+++ b/scripts/generateLayouts.ts
@@ -1,24 +1,21 @@
-import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFile, writeFileSync } from "fs"
-import Locale from "../src/UI/i18n/Locale"
-import Translations from "../src/UI/i18n/Translations"
-import { Translation } from "../src/UI/i18n/Translation"
-import all_known_layouts from "../src/assets/generated/known_themes.json"
-import { LayoutConfigJson } from "../src/Models/ThemeConfig/Json/LayoutConfigJson"
-import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig"
-import xml2js from "xml2js"
-import ScriptUtils from "./ScriptUtils"
-import { Utils } from "../src/Utils"
-import SpecialVisualizations from "../src/UI/SpecialVisualizations"
-import Constants from "../src/Models/Constants"
-import {
- AvailableRasterLayers,
- EditorLayerIndexProperties,
- RasterLayerPolygon,
-} from "../src/Models/RasterLayers"
-import { ImmutableStore } from "../src/Logic/UIEventSource"
-import * as crypto from "crypto"
-import * as eli from "../src/assets/editor-layer-index.json"
-import dom from "svelte/types/compiler/compile/render_dom"
+import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFile, writeFileSync } from "fs";
+import Locale from "../src/UI/i18n/Locale";
+import Translations from "../src/UI/i18n/Translations";
+import { Translation } from "../src/UI/i18n/Translation";
+import all_known_layouts from "../src/assets/generated/known_themes.json";
+import { LayoutConfigJson } from "../src/Models/ThemeConfig/Json/LayoutConfigJson";
+import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig";
+import xml2js from "xml2js";
+import ScriptUtils from "./ScriptUtils";
+import { Utils } from "../src/Utils";
+import SpecialVisualizations from "../src/UI/SpecialVisualizations";
+import Constants from "../src/Models/Constants";
+import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers";
+import { ImmutableStore } from "../src/Logic/UIEventSource";
+import * as crypto from "crypto";
+import * as eli from "../src/assets/editor-layer-index.json";
+import * as eli_global from "../src/assets/global-raster-layers.json";
+
const sharp = require("sharp")
const template = readFileSync("theme.html", "utf8")
const codeTemplate = readFileSync("src/index_theme.ts.template", "utf8")
@@ -219,7 +216,8 @@ function eliUrls(): string[] {
}
const urls: string[] = []
const regex = /{switch:([^}]+)}/
- for (const feature of eli.features) {
+ const rasterLayers = [...AvailableRasterLayers.vectorLayers, ...eli.features, ...eli_global.layers.map(properties => ({properties})) ]
+ for (const feature of rasterLayers) {
const url = (feature).properties.url
const match = url.match(regex)
if (match) {
@@ -245,8 +243,6 @@ function generateCsp(
...Constants.defaultOverpassUrls,
Constants.countryCoderEndpoint,
Constants.nominatimEndpoint,
- AvailableRasterLayers.maptilerCarto.properties.url,
- AvailableRasterLayers.maptilerDefaultLayer.properties.url,
"https://api.openstreetmap.org",
"https://pietervdvn.goatcounter.com",
]
diff --git a/scripts/hetzner/config/Caddyfile b/scripts/hetzner/config/Caddyfile
index f6fedc58e..139a80186 100644
--- a/scripts/hetzner/config/Caddyfile
+++ b/scripts/hetzner/config/Caddyfile
@@ -28,4 +28,3 @@ studio.mapcomplete.org {
to http://127.0.0.1:1235
}
}
-
diff --git a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts
index 8ab1faecf..ece8d156d 100644
--- a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts
+++ b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts
@@ -5,6 +5,7 @@ import { Feature, Point } from "geojson"
import { TagUtils } from "../../Tags/TagUtils"
import BaseUIElement from "../../../UI/BaseUIElement"
import { Utils } from "../../../Utils"
+import { OsmTags } from "../../../Models/OsmFeature"
/**
* Highly specialized feature source.
@@ -12,8 +13,14 @@ import { Utils } from "../../../Utils"
*/
export class LastClickFeatureSource implements WritableFeatureSource {
public readonly features: UIEventSource = new UIEventSource([])
+ public readonly hasNoteLayer: boolean
+ public readonly renderings: string[]
+ public readonly hasPresets: boolean
+ private i: number = 0
constructor(location: Store<{ lon: number; lat: number }>, layout: LayoutConfig) {
+ this.hasNoteLayer = layout.layers.some((l) => l.id === "note")
+ this.hasPresets = layout.layers.some((l) => l.presets?.length > 0)
const allPresets: BaseUIElement[] = []
for (const layer of layout.layers)
for (let i = 0; i < (layer.presets ?? []).length; i++) {
@@ -26,35 +33,36 @@ export class LastClickFeatureSource implements WritableFeatureSource {
allPresets.push(html)
}
- const renderings = Utils.Dedup(
+ this.renderings = Utils.Dedup(
allPresets.map((uiElem) =>
Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML
)
)
- let i = 0
-
location.addCallbackAndRunD(({ lon, lat }) => {
- const properties = {
- lastclick: "yes",
- id: "last_click_" + i,
- has_note_layer: layout.layers.some((l) => l.id === "note") ? "yes" : "no",
- has_presets: layout.layers.some((l) => l.presets?.length > 0) ? "yes" : "no",
- renderings: renderings.join(""),
- number_of_presets: "" + renderings.length,
- first_preset: renderings[0],
- }
- i++
-
- const point = >{
- type: "Feature",
- properties,
- geometry: {
- type: "Point",
- coordinates: [lon, lat],
- },
- }
- this.features.setData([point])
+ this.features.setData([this.createFeature(lon, lat)])
})
}
+
+ public createFeature(lon: number, lat: number): Feature {
+ const properties: OsmTags = {
+ lastclick: "yes",
+ id: "last_click_" + this.i,
+ has_note_layer: this.hasNoteLayer ? "yes" : "no",
+ has_presets: this.hasPresets ? "yes" : "no",
+ renderings: this.renderings.join(""),
+ number_of_presets: "" + this.renderings.length,
+ first_preset: this.renderings[0],
+ }
+ this.i++
+
+ return >{
+ type: "Feature",
+ properties,
+ geometry: {
+ type: "Point",
+ coordinates: [lon, lat],
+ },
+ }
+ }
}
diff --git a/src/Logic/State/FeatureSwitchState.ts b/src/Logic/State/FeatureSwitchState.ts
index 3fda13afd..a12835fbb 100644
--- a/src/Logic/State/FeatureSwitchState.ts
+++ b/src/Logic/State/FeatureSwitchState.ts
@@ -99,7 +99,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
)
this.featureSwitchCommunityIndex = FeatureSwitchUtils.initSwitch(
"fs-community-index",
- true,
+ this.featureSwitchEnableLogin.data,
"Disables/enables the button to get in touch with the community"
)
this.featureSwitchExtraLinkEnabled = FeatureSwitchUtils.initSwitch(
diff --git a/src/Logic/Web/ThemeViewStateHashActor.ts b/src/Logic/Web/ThemeViewStateHashActor.ts
index b10208f09..8527d8d79 100644
--- a/src/Logic/Web/ThemeViewStateHashActor.ts
+++ b/src/Logic/Web/ThemeViewStateHashActor.ts
@@ -175,7 +175,6 @@ export default class ThemeViewStateHashActor {
}
private back() {
- console.log("Got a back event")
const state = this._state
// history.pushState(null, null, window.location.pathname);
if (state.selectedElement.data) {
diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts
index ad68f6979..39aa182ad 100644
--- a/src/Models/Constants.ts
+++ b/src/Models/Constants.ts
@@ -58,7 +58,7 @@ export default class Constants {
importHelperUnlock: 5000,
}
- static readonly minZoomLevelToAddNewPoint = Constants.isRetina() ? 18 : 19
+ static readonly minZoomLevelToAddNewPoint = Constants.isRetina() ? 17 : 18
/**
* Used by 'PendingChangesUploader', which waits this amount of seconds to upload changes.
* (Note that pendingChanges might upload sooner if the popup is closed or similar)
diff --git a/src/Models/RasterLayers.ts b/src/Models/RasterLayers.ts
index 700a0cb30..9ef7f9250 100644
--- a/src/Models/RasterLayers.ts
+++ b/src/Models/RasterLayers.ts
@@ -1,172 +1,174 @@
-import { Feature, Polygon } from "geojson"
-import * as editorlayerindex from "../assets/editor-layer-index.json"
-import * as globallayers from "../assets/global-raster-layers.json"
-import { BBox } from "../Logic/BBox"
-import { Store, Stores } from "../Logic/UIEventSource"
-import { GeoOperations } from "../Logic/GeoOperations"
-import { RasterLayerProperties } from "./RasterLayerProperties"
+import { Feature, Polygon } from "geojson";
+import * as editorlayerindex from "../assets/editor-layer-index.json";
+import * as globallayers from "../assets/global-raster-layers.json";
+import { BBox } from "../Logic/BBox";
+import { Store, Stores } from "../Logic/UIEventSource";
+import { GeoOperations } from "../Logic/GeoOperations";
+import { RasterLayerProperties } from "./RasterLayerProperties";
export class AvailableRasterLayers {
- public static EditorLayerIndex: (Feature &
- RasterLayerPolygon)[] = editorlayerindex.features
- public static globalLayers: RasterLayerPolygon[] = globallayers.layers.map(
- (properties) =>
- {
- type: "Feature",
- properties,
- geometry: BBox.global.asGeometry(),
- }
- )
- public static readonly osmCartoProperties: RasterLayerProperties = {
- id: "osm",
- name: "OpenStreetMap",
- url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
- attribution: {
- text: "OpenStreetMap",
- url: "https://openStreetMap.org/copyright",
- },
- best: true,
- max_zoom: 19,
- min_zoom: 0,
- category: "osmbasedmap",
- }
-
- public static readonly osmCarto: RasterLayerPolygon = {
+ public static EditorLayerIndex: (Feature &
+ RasterLayerPolygon)[] = editorlayerindex.features;
+ public static globalLayers: RasterLayerPolygon[] = globallayers.layers.map(
+ (properties) =>
+ {
type: "Feature",
- properties: AvailableRasterLayers.osmCartoProperties,
- geometry: BBox.global.asGeometry(),
- }
+ properties,
+ geometry: BBox.global.asGeometry()
+ }
+ );
+ public static readonly osmCartoProperties: RasterLayerProperties = {
+ id: "osm",
+ name: "OpenStreetMap",
+ url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
+ attribution: {
+ text: "OpenStreetMap",
+ url: "https://openStreetMap.org/copyright"
+ },
+ best: true,
+ max_zoom: 19,
+ min_zoom: 0,
+ category: "osmbasedmap"
+ };
- public static readonly maptilerDefaultLayer: RasterLayerPolygon = {
- type: "Feature",
- properties: {
- name: "MapTiler",
- url: "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy",
- category: "osmbasedmap",
- id: "maptiler",
- type: "vector",
- attribution: {
- text: "Maptiler",
- url: "https://www.maptiler.com/copyright/",
- },
- },
- geometry: BBox.global.asGeometry(),
- }
+ public static readonly osmCarto: RasterLayerPolygon = {
+ type: "Feature",
+ properties: AvailableRasterLayers.osmCartoProperties,
+ geometry: BBox.global.asGeometry()
+ };
- public static readonly maptilerCarto: RasterLayerPolygon = {
- type: "Feature",
- properties: {
- name: "MapTiler Carto",
- url: "https://api.maptiler.com/maps/openstreetmap/style.json?key=GvoVAJgu46I5rZapJuAy",
- category: "osmbasedmap",
- id: "maptiler.carto",
- type: "vector",
- attribution: {
- text: "Maptiler",
- url: "https://www.maptiler.com/copyright/",
- },
- },
- geometry: BBox.global.asGeometry(),
- }
+ public static readonly maptilerDefaultLayer: RasterLayerPolygon = {
+ type: "Feature",
+ properties: {
+ name: "MapTiler",
+ url: "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy",
+ category: "osmbasedmap",
+ id: "maptiler",
+ type: "vector",
+ attribution: {
+ text: "Maptiler",
+ url: "https://www.maptiler.com/copyright/"
+ }
+ },
+ geometry: BBox.global.asGeometry()
+ };
- public static readonly maptilerBackdrop: RasterLayerPolygon = {
- type: "Feature",
- properties: {
- name: "MapTiler Backdrop",
- url: "https://api.maptiler.com/maps/backdrop/style.json?key=GvoVAJgu46I5rZapJuAy",
- category: "osmbasedmap",
- id: "maptiler.backdrop",
- type: "vector",
- attribution: {
- text: "Maptiler",
- url: "https://www.maptiler.com/copyright/",
- },
- },
- geometry: BBox.global.asGeometry(),
- }
- public static readonly americana: RasterLayerPolygon = {
- type: "Feature",
- properties: {
- name: "Americana",
- url: "https://zelonewolf.github.io/openstreetmap-americana/style.json",
- category: "osmbasedmap",
- id: "americana",
- type: "vector",
- attribution: {
- text: "Americana",
- url: "https://github.com/ZeLonewolf/openstreetmap-americana/",
- },
- },
- geometry: BBox.global.asGeometry(),
- }
+ public static readonly maptilerCarto: RasterLayerPolygon = {
+ type: "Feature",
+ properties: {
+ name: "MapTiler Carto",
+ url: "https://api.maptiler.com/maps/openstreetmap/style.json?key=GvoVAJgu46I5rZapJuAy",
+ category: "osmbasedmap",
+ id: "maptiler.carto",
+ type: "vector",
+ attribution: {
+ text: "Maptiler",
+ url: "https://www.maptiler.com/copyright/"
+ }
+ },
+ geometry: BBox.global.asGeometry()
+ };
- public static layersAvailableAt(
- location: Store<{ lon: number; lat: number }>
- ): Store {
- const availableLayersBboxes = Stores.ListStabilized(
- location.mapD((loc) => {
- const lonlat: [number, number] = [loc.lon, loc.lat]
- return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) =>
- BBox.get(eliPolygon).contains(lonlat)
- )
- })
- )
- const available = Stores.ListStabilized(
- availableLayersBboxes.map((eliPolygons) => {
- const loc = location.data
- const lonlat: [number, number] = [loc.lon, loc.lat]
- const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
- if (eliPolygon.geometry === null) {
- return true // global ELI-layer
- }
- return GeoOperations.inside(lonlat, eliPolygon)
- })
- matching.push(...AvailableRasterLayers.globalLayers)
- matching.unshift(
- AvailableRasterLayers.maptilerDefaultLayer,
- AvailableRasterLayers.osmCarto,
- AvailableRasterLayers.maptilerCarto,
- AvailableRasterLayers.maptilerBackdrop,
- AvailableRasterLayers.americana
- )
- return matching
- })
- )
- return available
- }
+ public static readonly maptilerBackdrop: RasterLayerPolygon = {
+ type: "Feature",
+ properties: {
+ name: "MapTiler Backdrop",
+ url: "https://api.maptiler.com/maps/backdrop/style.json?key=GvoVAJgu46I5rZapJuAy",
+ category: "osmbasedmap",
+ id: "maptiler.backdrop",
+ type: "vector",
+ attribution: {
+ text: "Maptiler",
+ url: "https://www.maptiler.com/copyright/"
+ }
+ },
+ geometry: BBox.global.asGeometry()
+ };
+ public static readonly americana: RasterLayerPolygon = {
+ type: "Feature",
+ properties: {
+ name: "Americana",
+ url: "https://zelonewolf.github.io/openstreetmap-americana/style.json",
+ category: "osmbasedmap",
+ id: "americana",
+ type: "vector",
+ attribution: {
+ text: "Americana",
+ url: "https://github.com/ZeLonewolf/openstreetmap-americana/"
+ }
+ },
+ geometry: BBox.global.asGeometry()
+ };
+
+ public static readonly vectorLayers = [
+ AvailableRasterLayers.maptilerDefaultLayer,
+ AvailableRasterLayers.osmCarto,
+ AvailableRasterLayers.maptilerCarto,
+ AvailableRasterLayers.maptilerBackdrop,
+ AvailableRasterLayers.americana
+ ];
+
+ public static layersAvailableAt(
+ location: Store<{ lon: number; lat: number }>
+ ): Store {
+ const availableLayersBboxes = Stores.ListStabilized(
+ location.mapD((loc) => {
+ const lonlat: [number, number] = [loc.lon, loc.lat];
+ return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) =>
+ BBox.get(eliPolygon).contains(lonlat)
+ );
+ })
+ );
+ const available = Stores.ListStabilized(
+ availableLayersBboxes.map((eliPolygons) => {
+ const loc = location.data;
+ const lonlat: [number, number] = [loc.lon, loc.lat];
+ const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
+ if (eliPolygon.geometry === null) {
+ return true; // global ELI-layer
+ }
+ return GeoOperations.inside(lonlat, eliPolygon);
+ });
+ matching.push(...AvailableRasterLayers.globalLayers);
+ matching.unshift(...AvailableRasterLayers.vectorLayers);
+ return matching;
+ })
+ );
+ return available;
+ }
}
export class RasterLayerUtils {
- /**
- * Selects, from the given list of available rasterLayerPolygons, a rasterLayer.
- * This rasterlayer will be of type 'preferredCategory' and will be of the 'best'-layer (if available).
- * Returns 'undefined' if no such layer is available
- * @param available
- * @param preferredCategory
- * @param ignoreLayer
- */
- public static SelectBestLayerAccordingTo(
- available: RasterLayerPolygon[],
- preferredCategory: string,
- ignoreLayer?: RasterLayerPolygon
- ): RasterLayerPolygon {
- let secondBest: RasterLayerPolygon = undefined
- for (const rasterLayer of available) {
- if (rasterLayer === ignoreLayer) {
- continue
- }
- const p = rasterLayer.properties
- if (p.category === preferredCategory) {
- if (p.best) {
- return rasterLayer
- }
- if (!secondBest) {
- secondBest = rasterLayer
- }
- }
+ /**
+ * Selects, from the given list of available rasterLayerPolygons, a rasterLayer.
+ * This rasterlayer will be of type 'preferredCategory' and will be of the 'best'-layer (if available).
+ * Returns 'undefined' if no such layer is available
+ * @param available
+ * @param preferredCategory
+ * @param ignoreLayer
+ */
+ public static SelectBestLayerAccordingTo(
+ available: RasterLayerPolygon[],
+ preferredCategory: string,
+ ignoreLayer?: RasterLayerPolygon
+ ): RasterLayerPolygon {
+ let secondBest: RasterLayerPolygon = undefined;
+ for (const rasterLayer of available) {
+ if (rasterLayer === ignoreLayer) {
+ continue;
+ }
+ const p = rasterLayer.properties;
+ if (p.category === preferredCategory) {
+ if (p.best) {
+ return rasterLayer;
}
- return secondBest
+ if (!secondBest) {
+ secondBest = rasterLayer;
+ }
+ }
}
+ return secondBest;
+ }
}
export type RasterLayerPolygon = Feature
@@ -178,165 +180,165 @@ export type RasterLayerPolygon = Feature
* which was then converted with http://borischerny.com/json-schema-to-typescript-browser/
*/
export interface EditorLayerIndexProperties extends RasterLayerProperties {
+ /**
+ * The name of the imagery source
+ */
+ readonly name: string;
+ /**
+ * Whether the imagery name should be translated
+ */
+ readonly i18n?: boolean;
+ readonly type:
+ | "tms"
+ | "wms"
+ | "bing"
+ | "scanex"
+ | "wms_endpoint"
+ | "wmts"
+ | "vector"; /* Vector is not actually part of the ELI-spec, we add it for vector layers */
+ /**
+ * A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories.
+ */
+ readonly category?:
+ | "photo"
+ | "map"
+ | "historicmap"
+ | "osmbasedmap"
+ | "historicphoto"
+ | "qa"
+ | "elevation"
+ | "other";
+ /**
+ * A URL template for imagery tiles
+ */
+ readonly url: string;
+ readonly min_zoom?: number;
+ readonly max_zoom?: number;
+ /**
+ * explicit/implicit permission by the owner for use in OSM
+ */
+ readonly permission_osm?: "explicit" | "implicit" | "no";
+ /**
+ * A URL for the license or permissions for the imagery
+ */
+ readonly license_url?: string;
+ /**
+ * A URL for the privacy policy of the operator or false if there is no existing privacy policy for tis imagery.
+ */
+ readonly privacy_policy_url?: string | boolean;
+ /**
+ * A unique identifier for the source; used in imagery_used changeset tag
+ */
+ readonly id: string;
+ /**
+ * A short English-language description of the source
+ */
+ readonly description?: string;
+ /**
+ * The ISO 3166-1 alpha-2 two letter country code in upper case. Use ZZ for unknown or multiple.
+ */
+ readonly country_code?: string;
+ /**
+ * Whether this imagery should be shown in the default world-wide menu
+ */
+ readonly default?: boolean;
+ /**
+ * Whether this imagery is the best source for the region
+ */
+ readonly best?: boolean;
+ /**
+ * The age of the oldest imagery or data in the source, as an RFC3339 date or leading portion of one
+ */
+ readonly start_date?: string;
+ /**
+ * The age of the newest imagery or data in the source, as an RFC3339 date or leading portion of one
+ */
+ readonly end_date?: string;
+ /**
+ * HTTP header to check for information if the tile is invalid
+ */
+ readonly no_tile_header?: {
/**
- * The name of the imagery source
+ * This interface was referenced by `undefined`'s JSON-Schema definition
+ * via the `patternProperty` "^.*$".
*/
- readonly name: string
- /**
- * Whether the imagery name should be translated
- */
- readonly i18n?: boolean
- readonly type:
- | "tms"
- | "wms"
- | "bing"
- | "scanex"
- | "wms_endpoint"
- | "wmts"
- | "vector" /* Vector is not actually part of the ELI-spec, we add it for vector layers */
- /**
- * A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories.
- */
- readonly category?:
- | "photo"
- | "map"
- | "historicmap"
- | "osmbasedmap"
- | "historicphoto"
- | "qa"
- | "elevation"
- | "other"
- /**
- * A URL template for imagery tiles
- */
- readonly url: string
- readonly min_zoom?: number
- readonly max_zoom?: number
- /**
- * explicit/implicit permission by the owner for use in OSM
- */
- readonly permission_osm?: "explicit" | "implicit" | "no"
- /**
- * A URL for the license or permissions for the imagery
- */
- readonly license_url?: string
- /**
- * A URL for the privacy policy of the operator or false if there is no existing privacy policy for tis imagery.
- */
- readonly privacy_policy_url?: string | boolean
- /**
- * A unique identifier for the source; used in imagery_used changeset tag
- */
- readonly id: string
- /**
- * A short English-language description of the source
- */
- readonly description?: string
- /**
- * The ISO 3166-1 alpha-2 two letter country code in upper case. Use ZZ for unknown or multiple.
- */
- readonly country_code?: string
- /**
- * Whether this imagery should be shown in the default world-wide menu
- */
- readonly default?: boolean
- /**
- * Whether this imagery is the best source for the region
- */
- readonly best?: boolean
- /**
- * The age of the oldest imagery or data in the source, as an RFC3339 date or leading portion of one
- */
- readonly start_date?: string
- /**
- * The age of the newest imagery or data in the source, as an RFC3339 date or leading portion of one
- */
- readonly end_date?: string
- /**
- * HTTP header to check for information if the tile is invalid
- */
- readonly no_tile_header?: {
- /**
- * This interface was referenced by `undefined`'s JSON-Schema definition
- * via the `patternProperty` "^.*$".
- */
- [k: string]: string[] | null
+ [k: string]: string[] | null
+ };
+ /**
+ * 'true' if tiles are transparent and can be overlaid on another source
+ */
+ readonly overlay?: boolean & string;
+ readonly available_projections?: string[];
+ readonly attribution?: {
+ readonly url?: string
+ readonly text?: string
+ readonly html?: string
+ readonly required?: boolean
+ };
+ /**
+ * A URL for an image, that can be displayed in the list of imagery layers next to the name
+ */
+ readonly icon?: string;
+ /**
+ * A link to an EULA text that has to be accepted by the user, before the imagery source is added. Can contain {lang} to be replaced by a current user language wiki code (like FR:) or an empty string for the default English text.
+ */
+ readonly eula?: string;
+ /**
+ * A URL for an image, that is displayed in the mapview for attribution
+ */
+ readonly "logo-image"?: string;
+ /**
+ * Customized text for the terms of use link (default is "Background Terms of Use")
+ */
+ readonly "terms-of-use-text"?: string;
+ /**
+ * Specify a checksum for tiles, which aren't real tiles. `type` is the digest type and can be MD5, SHA-1, SHA-256, SHA-384 and SHA-512, value is the hex encoded checksum in lower case. To create a checksum save the tile as file and upload it to e.g. https://defuse.ca/checksums.htm.
+ */
+ readonly "no-tile-checksum"?: string;
+ /**
+ * header-name attribute specifies a header returned by tile server, that will be shown as `metadata-key` attribute in Show Tile Info dialog
+ */
+ readonly "metadata-header"?: string;
+ /**
+ * Set to `true` if imagery source is properly aligned and does not need imagery offset adjustments. This is used for OSM based sources too.
+ */
+ readonly "valid-georeference"?: boolean;
+ /**
+ * Size of individual tiles delivered by a TMS service
+ */
+ readonly "tile-size"?: number;
+ /**
+ * Whether tiles status can be accessed by appending /status to the tile URL and can be submitted for re-rendering by appending /dirty.
+ */
+ readonly "mod-tile-features"?: string;
+ /**
+ * HTTP headers to be sent to server. It has two attributes header-name and header-value. May be specified multiple times.
+ */
+ readonly "custom-http-headers"?: {
+ readonly "header-name"?: string
+ readonly "header-value"?: string
+ };
+ /**
+ * Default layer to open (when using WMS_ENDPOINT type). Contains list of layer tag with two attributes - name and style, e.g. `"default-layers": ["layer": { name="Basisdata_NP_Basiskart_JanMayen_WMTS_25829" "style":"default" } ]` (not allowed in `mirror` attribute)
+ */
+ readonly "default-layers"?: {
+ layer?: {
+ "layer-name"?: string
+ "layer-style"?: string
+ [k: string]: unknown
}
- /**
- * 'true' if tiles are transparent and can be overlaid on another source
- */
- readonly overlay?: boolean & string
- readonly available_projections?: string[]
- readonly attribution?: {
- readonly url?: string
- readonly text?: string
- readonly html?: string
- readonly required?: boolean
- }
- /**
- * A URL for an image, that can be displayed in the list of imagery layers next to the name
- */
- readonly icon?: string
- /**
- * A link to an EULA text that has to be accepted by the user, before the imagery source is added. Can contain {lang} to be replaced by a current user language wiki code (like FR:) or an empty string for the default English text.
- */
- readonly eula?: string
- /**
- * A URL for an image, that is displayed in the mapview for attribution
- */
- readonly "logo-image"?: string
- /**
- * Customized text for the terms of use link (default is "Background Terms of Use")
- */
- readonly "terms-of-use-text"?: string
- /**
- * Specify a checksum for tiles, which aren't real tiles. `type` is the digest type and can be MD5, SHA-1, SHA-256, SHA-384 and SHA-512, value is the hex encoded checksum in lower case. To create a checksum save the tile as file and upload it to e.g. https://defuse.ca/checksums.htm.
- */
- readonly "no-tile-checksum"?: string
- /**
- * header-name attribute specifies a header returned by tile server, that will be shown as `metadata-key` attribute in Show Tile Info dialog
- */
- readonly "metadata-header"?: string
- /**
- * Set to `true` if imagery source is properly aligned and does not need imagery offset adjustments. This is used for OSM based sources too.
- */
- readonly "valid-georeference"?: boolean
- /**
- * Size of individual tiles delivered by a TMS service
- */
- readonly "tile-size"?: number
- /**
- * Whether tiles status can be accessed by appending /status to the tile URL and can be submitted for re-rendering by appending /dirty.
- */
- readonly "mod-tile-features"?: string
- /**
- * HTTP headers to be sent to server. It has two attributes header-name and header-value. May be specified multiple times.
- */
- readonly "custom-http-headers"?: {
- readonly "header-name"?: string
- readonly "header-value"?: string
- }
- /**
- * Default layer to open (when using WMS_ENDPOINT type). Contains list of layer tag with two attributes - name and style, e.g. `"default-layers": ["layer": { name="Basisdata_NP_Basiskart_JanMayen_WMTS_25829" "style":"default" } ]` (not allowed in `mirror` attribute)
- */
- readonly "default-layers"?: {
- layer?: {
- "layer-name"?: string
- "layer-style"?: string
- [k: string]: unknown
- }
- [k: string]: unknown
- }[]
- /**
- * format to use when connecting tile server (when using WMS_ENDPOINT type)
- */
- readonly format?: string
- /**
- * If `true` transparent tiles will be requested from WMS server
- */
- readonly transparent?: boolean & string
- /**
- * minimum expiry time for tiles in seconds. The larger the value, the longer entry in cache will be considered valid
- */
- readonly "minimum-tile-expire"?: number
+ [k: string]: unknown
+ }[];
+ /**
+ * format to use when connecting tile server (when using WMS_ENDPOINT type)
+ */
+ readonly format?: string;
+ /**
+ * If `true` transparent tiles will be requested from WMS server
+ */
+ readonly transparent?: boolean & string;
+ /**
+ * minimum expiry time for tiles in seconds. The larger the value, the longer entry in cache will be considered valid
+ */
+ readonly "minimum-tile-expire"?: number;
}
diff --git a/src/Models/ThemeConfig/LayoutConfig.ts b/src/Models/ThemeConfig/LayoutConfig.ts
index f9b3c2d5f..d399ed5ae 100644
--- a/src/Models/ThemeConfig/LayoutConfig.ts
+++ b/src/Models/ThemeConfig/LayoutConfig.ts
@@ -95,6 +95,9 @@ export default class LayoutConfig implements LayoutInformation {
}
const context = this.id
this.credits = json.credits
+ if(!json.title){
+ throw `The theme ${json.id} does not have a title defined.`
+ }
this.language = json.mustHaveLanguage ?? Object.keys(json.title)
this.usedImages = Array.from(
new ExtractImages(official, undefined)
diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts
index a15f6b056..f52da26f5 100644
--- a/src/Models/ThemeViewState.ts
+++ b/src/Models/ThemeViewState.ts
@@ -1,62 +1,58 @@
-import LayoutConfig from "./ThemeConfig/LayoutConfig"
-import { SpecialVisualizationState } from "../UI/SpecialVisualization"
-import { Changes } from "../Logic/Osm/Changes"
-import { Store, UIEventSource } from "../Logic/UIEventSource"
+import LayoutConfig from "./ThemeConfig/LayoutConfig";
+import { SpecialVisualizationState } from "../UI/SpecialVisualization";
+import { Changes } from "../Logic/Osm/Changes";
+import { Store, UIEventSource } from "../Logic/UIEventSource";
+import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource";
+import { OsmConnection } from "../Logic/Osm/OsmConnection";
+import { ExportableMap, MapProperties } from "./MapProperties";
+import LayerState from "../Logic/State/LayerState";
+import { Feature, Point, Polygon } from "geojson";
+import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
+import { Map as MlMap } from "maplibre-gl";
+import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning";
+import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor";
+import { GeoLocationState } from "../Logic/State/GeoLocationState";
+import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
+import { QueryParameters } from "../Logic/Web/QueryParameters";
+import UserRelatedState from "../Logic/State/UserRelatedState";
+import LayerConfig from "./ThemeConfig/LayerConfig";
+import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler";
+import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers";
+import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource";
+import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
+import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore";
+import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
+import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource";
+import ShowDataLayer from "../UI/Map/ShowDataLayer";
+import TitleHandler from "../Logic/Actors/TitleHandler";
+import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor";
+import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader";
+import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater";
+import { BBox } from "../Logic/BBox";
+import Constants from "./Constants";
+import Hotkeys from "../UI/Base/Hotkeys";
+import Translations from "../UI/i18n/Translations";
+import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore";
+import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource";
+import { MenuState } from "./MenuState";
+import MetaTagging from "../Logic/MetaTagging";
+import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator";
import {
- FeatureSource,
- IndexedFeatureSource,
- WritableFeatureSource,
-} from "../Logic/FeatureSource/FeatureSource"
-import { OsmConnection } from "../Logic/Osm/OsmConnection"
-import { ExportableMap, MapProperties } from "./MapProperties"
-import LayerState from "../Logic/State/LayerState"
-import { Feature, Point, Polygon } from "geojson"
-import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
-import { Map as MlMap } from "maplibre-gl"
-import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"
-import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor"
-import { GeoLocationState } from "../Logic/State/GeoLocationState"
-import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
-import { QueryParameters } from "../Logic/Web/QueryParameters"
-import UserRelatedState from "../Logic/State/UserRelatedState"
-import LayerConfig from "./ThemeConfig/LayerConfig"
-import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
-import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers"
-import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
-import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
-import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
-import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
-import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
-import ShowDataLayer from "../UI/Map/ShowDataLayer"
-import TitleHandler from "../Logic/Actors/TitleHandler"
-import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor"
-import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader"
-import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater"
-import { BBox } from "../Logic/BBox"
-import Constants from "./Constants"
-import Hotkeys from "../UI/Base/Hotkeys"
-import Translations from "../UI/i18n/Translations"
-import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
-import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
-import { MenuState } from "./MenuState"
-import MetaTagging from "../Logic/MetaTagging"
-import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"
-import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
-import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
-import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer"
-import { Utils } from "../Utils"
-import { EliCategory } from "./RasterLayerProperties"
-import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"
-import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
-import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
-import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"
-import NoElementsInViewDetector, {
- FeatureViewState,
-} from "../Logic/Actors/NoElementsInViewDetector"
-import FilteredLayer from "./FilteredLayer"
-import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
-import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
-import { Imgur } from "../Logic/ImageProviders/Imgur"
+ NewGeometryFromChangesFeatureSource
+} from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource";
+import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader";
+import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer";
+import { Utils } from "../Utils";
+import { EliCategory } from "./RasterLayerProperties";
+import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter";
+import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage";
+import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource";
+import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor";
+import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector";
+import FilteredLayer from "./FilteredLayer";
+import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector";
+import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager";
+import { Imgur } from "../Logic/ImageProviders/Imgur";
/**
*
@@ -67,559 +63,581 @@ import { Imgur } from "../Logic/ImageProviders/Imgur"
* It ties up all the needed elements and starts some actors.
*/
export default class ThemeViewState implements SpecialVisualizationState {
- readonly layout: LayoutConfig
- readonly map: UIEventSource
- readonly changes: Changes
- readonly featureSwitches: FeatureSwitchState
- readonly featureSwitchIsTesting: Store
- readonly featureSwitchUserbadge: Store
+ readonly layout: LayoutConfig;
+ readonly map: UIEventSource;
+ readonly changes: Changes;
+ readonly featureSwitches: FeatureSwitchState;
+ readonly featureSwitchIsTesting: Store;
+ readonly featureSwitchUserbadge: Store;
- readonly featureProperties: FeaturePropertiesStore
+ readonly featureProperties: FeaturePropertiesStore;
- readonly osmConnection: OsmConnection
- readonly selectedElement: UIEventSource
- readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
- readonly mapProperties: MapProperties & ExportableMap
- readonly osmObjectDownloader: OsmObjectDownloader
+ readonly osmConnection: OsmConnection;
+ readonly selectedElement: UIEventSource;
+ readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>;
+ readonly mapProperties: MapProperties & ExportableMap;
+ readonly osmObjectDownloader: OsmObjectDownloader;
- readonly dataIsLoading: Store
- /**
- * Indicates if there is _some_ data in view, even if it is not shown due to the filters
- */
- readonly hasDataInView: Store
+ readonly dataIsLoading: Store;
+ /**
+ * Indicates if there is _some_ data in view, even if it is not shown due to the filters
+ */
+ readonly hasDataInView: Store;
- readonly guistate: MenuState
- readonly fullNodeDatabase?: FullNodeDatabaseSource
+ readonly guistate: MenuState;
+ readonly fullNodeDatabase?: FullNodeDatabaseSource;
- readonly historicalUserLocations: WritableFeatureSource>
- readonly indexedFeatures: IndexedFeatureSource & LayoutSource
- readonly currentView: FeatureSource>
- readonly featuresInView: FeatureSource
- readonly newFeatures: WritableFeatureSource
- readonly layerState: LayerState
- readonly perLayer: ReadonlyMap
- readonly perLayerFiltered: ReadonlyMap
+ readonly historicalUserLocations: WritableFeatureSource>;
+ readonly indexedFeatures: IndexedFeatureSource & LayoutSource;
+ readonly currentView: FeatureSource>;
+ readonly featuresInView: FeatureSource;
+ readonly newFeatures: WritableFeatureSource;
+ readonly layerState: LayerState;
+ readonly perLayer: ReadonlyMap;
+ readonly perLayerFiltered: ReadonlyMap;
- readonly availableLayers: Store
- readonly selectedLayer: UIEventSource
- readonly userRelatedState: UserRelatedState
- readonly geolocation: GeoLocationHandler
+ readonly availableLayers: Store;
+ readonly selectedLayer: UIEventSource;
+ readonly userRelatedState: UserRelatedState;
+ readonly geolocation: GeoLocationHandler;
- readonly imageUploadManager: ImageUploadManager
+ readonly imageUploadManager: ImageUploadManager;
- readonly lastClickObject: WritableFeatureSource
- readonly overlayLayerStates: ReadonlyMap<
- string,
- { readonly isDisplayed: UIEventSource }
- >
- /**
- * All 'level'-tags that are available with the current features
- */
- readonly floors: Store
+ readonly addNewPoint: UIEventSource = new UIEventSource(false);
- constructor(layout: LayoutConfig) {
- Utils.initDomPurify()
- this.layout = layout
- this.featureSwitches = new FeatureSwitchState(layout)
- this.guistate = new MenuState(
- this.featureSwitches.featureSwitchWelcomeMessage.data,
- layout.id
- )
- this.map = new UIEventSource(undefined)
- const initial = new InitialMapPositioning(layout)
- this.mapProperties = new MapLibreAdaptor(this.map, initial)
- const geolocationState = new GeoLocationState()
+ readonly lastClickObject: LastClickFeatureSource;
+ readonly overlayLayerStates: ReadonlyMap<
+ string,
+ { readonly isDisplayed: UIEventSource }
+ >;
+ /**
+ * All 'level'-tags that are available with the current features
+ */
+ readonly floors: Store;
+ private readonly newPointDialog: FilteredLayer;
- this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
- this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
+ constructor(layout: LayoutConfig) {
+ Utils.initDomPurify();
+ this.layout = layout;
+ this.featureSwitches = new FeatureSwitchState(layout);
+ this.guistate = new MenuState(
+ this.featureSwitches.featureSwitchWelcomeMessage.data,
+ layout.id
+ );
+ this.map = new UIEventSource(undefined);
+ const initial = new InitialMapPositioning(layout);
+ this.mapProperties = new MapLibreAdaptor(this.map, initial);
+ const geolocationState = new GeoLocationState();
- this.osmConnection = new OsmConnection({
- dryRun: this.featureSwitches.featureSwitchIsTesting,
- fakeUser: this.featureSwitches.featureSwitchFakeUser.data,
- oauth_token: QueryParameters.GetQueryParameter(
- "oauth_token",
- undefined,
- "Used to complete the login"
- ),
- })
- this.userRelatedState = new UserRelatedState(
- this.osmConnection,
- layout?.language,
- layout,
- this.featureSwitches,
- this.mapProperties
- )
- this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => {
- this.mapProperties.allowRotating.setData(fixated !== "yes")
- })
- this.selectedElement = new UIEventSource(undefined, "Selected element")
- this.selectedLayer = new UIEventSource(undefined, "Selected layer")
+ this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting;
+ this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin;
- this.selectedElementAndLayer = this.selectedElement.mapD(
- (feature) => {
- const layer = this.selectedLayer.data
- if (!layer) {
- return undefined
- }
- return { layer, feature }
- },
- [this.selectedLayer]
- )
+ this.osmConnection = new OsmConnection({
+ dryRun: this.featureSwitches.featureSwitchIsTesting,
+ fakeUser: this.featureSwitches.featureSwitchFakeUser.data,
+ oauth_token: QueryParameters.GetQueryParameter(
+ "oauth_token",
+ undefined,
+ "Used to complete the login"
+ )
+ });
+ this.userRelatedState = new UserRelatedState(
+ this.osmConnection,
+ layout?.language,
+ layout,
+ this.featureSwitches,
+ this.mapProperties
+ );
+ this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => {
+ this.mapProperties.allowRotating.setData(fixated !== "yes");
+ });
+ this.selectedElement = new UIEventSource(undefined, "Selected element");
+ this.selectedLayer = new UIEventSource(undefined, "Selected layer");
- this.geolocation = new GeoLocationHandler(
- geolocationState,
- this.selectedElement,
- this.mapProperties,
- this.userRelatedState.gpsLocationHistoryRetentionTime
- )
-
- this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
-
- const self = this
- this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
-
- {
- const overlayLayerStates = new Map }>()
- for (const rasterInfo of this.layout.tileLayerSources) {
- const isDisplayed = QueryParameters.GetBooleanQueryParameter(
- "overlay-" + rasterInfo.id,
- rasterInfo.defaultState ?? true,
- "Wether or not overlayer layer " + rasterInfo.id + " is shown"
- )
- const state = { isDisplayed }
- overlayLayerStates.set(rasterInfo.id, state)
- new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state)
- }
- this.overlayLayerStates = overlayLayerStates
+ this.selectedElementAndLayer = this.selectedElement.mapD(
+ (feature) => {
+ const layer = this.selectedLayer.data;
+ if (!layer) {
+ return undefined;
}
+ return { layer, feature };
+ },
+ [this.selectedLayer]
+ );
- {
- /* Setup the layout source
- * A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too
- */
+ this.geolocation = new GeoLocationHandler(
+ geolocationState,
+ this.selectedElement,
+ this.mapProperties,
+ this.userRelatedState.gpsLocationHistoryRetentionTime
+ );
- if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) {
- this.fullNodeDatabase = new FullNodeDatabaseSource()
- }
+ this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location);
- const layoutSource = new LayoutSource(
- layout.layers,
- this.featureSwitches,
- this.mapProperties,
- this.osmConnection.Backend(),
- (id) => self.layerState.filteredLayers.get(id).isDisplayed,
- this.fullNodeDatabase
- )
+ const self = this;
+ this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id);
- this.indexedFeatures = layoutSource
+ {
+ const overlayLayerStates = new Map }>();
+ for (const rasterInfo of this.layout.tileLayerSources) {
+ const isDisplayed = QueryParameters.GetBooleanQueryParameter(
+ "overlay-" + rasterInfo.id,
+ rasterInfo.defaultState ?? true,
+ "Wether or not overlayer layer " + rasterInfo.id + " is shown"
+ );
+ const state = { isDisplayed };
+ overlayLayerStates.set(rasterInfo.id, state);
+ new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state);
+ }
+ this.overlayLayerStates = overlayLayerStates;
+ }
- const empty = []
- let currentViewIndex = 0
- this.currentView = new StaticFeatureSource(
- this.mapProperties.bounds.map((bbox) => {
- if (!bbox) {
- return empty
- }
- currentViewIndex++
- return [
- bbox.asGeoJson({
- zoom: this.mapProperties.zoom.data,
- ...this.mapProperties.location.data,
- id: "current_view",
- }),
- ]
- })
- )
- this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
- this.dataIsLoading = layoutSource.isLoading
+ {
+ /* Setup the layout source
+ * A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too
+ */
- const indexedElements = this.indexedFeatures
- this.featureProperties = new FeaturePropertiesStore(indexedElements)
- this.changes = new Changes(
- {
- dryRun: this.featureSwitches.featureSwitchIsTesting,
- allElements: indexedElements,
- featurePropertiesStore: this.featureProperties,
- osmConnection: this.osmConnection,
- historicalUserLocations: this.geolocation.historicalUserLocations,
- },
- layout?.isLeftRightSensitive() ?? false
- )
- this.historicalUserLocations = this.geolocation.historicalUserLocations
- this.newFeatures = new NewGeometryFromChangesFeatureSource(
- this.changes,
- indexedElements,
- this.featureProperties
- )
- layoutSource.addSource(this.newFeatures)
+ if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) {
+ this.fullNodeDatabase = new FullNodeDatabaseSource();
+ }
- const perLayer = new PerLayerFeatureSourceSplitter(
- Array.from(this.layerState.filteredLayers.values()).filter(
- (l) => l.layerDef?.source !== null
- ),
- new ChangeGeometryApplicator(this.indexedFeatures, this.changes),
- {
- constructStore: (features, layer) =>
- new GeoIndexedStoreForLayer(features, layer),
- handleLeftovers: (features) => {
- console.warn(
- "Got ",
- features.length,
- "leftover features, such as",
- features[0].properties
- )
- },
- }
- )
- this.perLayer = perLayer.perLayer
- }
- this.perLayer.forEach((fs) => {
- new SaveFeatureSourceToLocalStorage(
- this.osmConnection.Backend(),
- fs.layer.layerDef.id,
- 15,
- fs,
- this.featureProperties,
- fs.layer.layerDef.maxAgeOfCache
- )
- })
+ const layoutSource = new LayoutSource(
+ layout.layers,
+ this.featureSwitches,
+ this.mapProperties,
+ this.osmConnection.Backend(),
+ (id) => self.layerState.filteredLayers.get(id).isDisplayed,
+ this.fullNodeDatabase
+ );
- this.floors = this.featuresInView.features.stabilized(500).map((features) => {
- if (!features) {
- return []
- }
- const floors = new Set()
- for (const feature of features) {
- let level = feature.properties["_level"]
- if (level) {
- const levels = level.split(";")
- for (const l of levels) {
- floors.add(l)
- }
- } else {
- floors.add("0") // '0' is the default and is thus _always_ present
- }
- }
- const sorted = Array.from(floors)
- // Sort alphabetically first, to deal with floor "A", "B" and "C"
- sorted.sort()
- sorted.sort((a, b) => {
- // We use the laxer 'parseInt' to deal with floor '1A'
- const na = parseInt(a)
- const nb = parseInt(b)
- if (isNaN(na) || isNaN(nb)) {
- return 0
- }
- return na - nb
+ this.indexedFeatures = layoutSource;
+
+ let currentViewIndex = 0
+ const empty = [];
+ this.currentView = new StaticFeatureSource(
+ this.mapProperties.bounds.map((bbox) => {
+ if (!bbox) {
+ return empty;
+ }
+ currentViewIndex++;
+ return [
+ bbox.asGeoJson({
+ zoom: this.mapProperties.zoom.data,
+ ...this.mapProperties.location.data,
+ id: "current_view_"+currentViewIndex
})
- sorted.reverse(/* new list, no side-effects */)
- return sorted
+ ];
})
+ );
+ this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds);
+ this.dataIsLoading = layoutSource.isLoading;
- const lastClick = (this.lastClickObject = new LastClickFeatureSource(
- this.mapProperties.lastClickLocation,
- this.layout
- ))
+ const indexedElements = this.indexedFeatures;
+ this.featureProperties = new FeaturePropertiesStore(indexedElements);
+ this.changes = new Changes(
+ {
+ dryRun: this.featureSwitches.featureSwitchIsTesting,
+ allElements: indexedElements,
+ featurePropertiesStore: this.featureProperties,
+ osmConnection: this.osmConnection,
+ historicalUserLocations: this.geolocation.historicalUserLocations
+ },
+ layout?.isLeftRightSensitive() ?? false
+ );
+ this.historicalUserLocations = this.geolocation.historicalUserLocations;
+ this.newFeatures = new NewGeometryFromChangesFeatureSource(
+ this.changes,
+ indexedElements,
+ this.featureProperties
+ );
+ layoutSource.addSource(this.newFeatures);
- this.osmObjectDownloader = new OsmObjectDownloader(
- this.osmConnection.Backend(),
- this.changes
- )
-
- this.perLayerFiltered = this.showNormalDataOn(this.map)
-
- this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
- this.imageUploadManager = new ImageUploadManager(
- layout,
- Imgur.singleton,
- this.featureProperties,
- this.osmConnection,
- this.changes
- )
-
- this.initActors()
- this.addLastClick(lastClick)
- this.drawSpecialLayers()
- this.initHotkeys()
- this.miscSetup()
- if (!Utils.runningFromConsole) {
- console.log("State setup completed", this)
+ const perLayer = new PerLayerFeatureSourceSplitter(
+ Array.from(this.layerState.filteredLayers.values()).filter(
+ (l) => l.layerDef?.source !== null
+ ),
+ new ChangeGeometryApplicator(this.indexedFeatures, this.changes),
+ {
+ constructStore: (features, layer) =>
+ new GeoIndexedStoreForLayer(features, layer),
+ handleLeftovers: (features) => {
+ console.warn(
+ "Got ",
+ features.length,
+ "leftover features, such as",
+ features[0].properties
+ );
+ }
}
+ );
+ this.perLayer = perLayer.perLayer;
}
+ this.perLayer.forEach((fs) => {
+ new SaveFeatureSourceToLocalStorage(
+ this.osmConnection.Backend(),
+ fs.layer.layerDef.id,
+ 15,
+ fs,
+ this.featureProperties,
+ fs.layer.layerDef.maxAgeOfCache
+ );
+ });
+ this.newPointDialog = this.layerState.filteredLayers.get("last_click");
- public showNormalDataOn(map: Store): ReadonlyMap {
- const filteringFeatureSource = new Map()
- this.perLayer.forEach((fs, layerName) => {
- const doShowLayer = this.mapProperties.zoom.map(
- (z) =>
- (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
- [fs.layer.isDisplayed]
- )
-
- if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) {
- /* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined)
- *
- * This means that we don't have to filter it, nor do we have to display it
- *
- * Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden.
- * However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer!
- * */
- return
- }
- const filtered = new FilteringFeatureSource(
- fs.layer,
- fs,
- (id) => this.featureProperties.getStore(id),
- this.layerState.globalFilters
- )
- filteringFeatureSource.set(layerName, filtered)
-
- new ShowDataLayer(map, {
- layer: fs.layer.layerDef,
- features: filtered,
- doShowLayer,
- selectedElement: this.selectedElement,
- selectedLayer: this.selectedLayer,
- fetchStore: (id) => this.featureProperties.getStore(id),
- })
- })
- return filteringFeatureSource
- }
-
- /**
- * Various small methods that need to be called
- */
- private miscSetup() {
- this.userRelatedState.markLayoutAsVisited(this.layout)
-
- this.selectedElement.addCallbackAndRunD((feature) => {
- // As soon as we have a selected element, we clear the selected element
- // This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature
- // The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear
- if (feature.properties.id === "last_click") {
- return
- }
- this.lastClickObject.features.setData([])
- })
-
- if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) {
- Utils.LoadCustomCss(this.layout.customCss)
+ this.floors = this.featuresInView.features.stabilized(500).map((features) => {
+ if (!features) {
+ return [];
+ }
+ const floors = new Set();
+ for (const feature of features) {
+ let level = feature.properties["_level"];
+ if (level) {
+ const levels = level.split(";");
+ for (const l of levels) {
+ floors.add(l);
+ }
+ } else {
+ floors.add("0"); // '0' is the default and is thus _always_ present
}
+ }
+ const sorted = Array.from(floors);
+ // Sort alphabetically first, to deal with floor "A", "B" and "C"
+ sorted.sort();
+ sorted.sort((a, b) => {
+ // We use the laxer 'parseInt' to deal with floor '1A'
+ const na = parseInt(a);
+ const nb = parseInt(b);
+ if (isNaN(na) || isNaN(nb)) {
+ return 0;
+ }
+ return na - nb;
+ });
+ sorted.reverse(/* new list, no side-effects */);
+ return sorted;
+ });
+
+ const lastClick = (this.lastClickObject = new LastClickFeatureSource(
+ this.mapProperties.lastClickLocation,
+ this.layout
+ ));
+
+ this.osmObjectDownloader = new OsmObjectDownloader(
+ this.osmConnection.Backend(),
+ this.changes
+ );
+
+ this.perLayerFiltered = this.showNormalDataOn(this.map);
+
+ this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView;
+ this.imageUploadManager = new ImageUploadManager(
+ layout,
+ Imgur.singleton,
+ this.featureProperties,
+ this.osmConnection,
+ this.changes
+ );
+
+ this.initActors();
+ this.drawSpecialLayers();
+ this.initHotkeys();
+ this.miscSetup();
+ if (!Utils.runningFromConsole) {
+ console.log("State setup completed", this);
}
+ }
- private initHotkeys() {
- Hotkeys.RegisterHotkey(
- { nomod: "Escape", onUp: true },
- Translations.t.hotkeyDocumentation.closeSidebar,
- () => {
- this.selectedElement.setData(undefined)
- this.guistate.closeAll()
- }
- )
+ public showNormalDataOn(map: Store): ReadonlyMap {
+ const filteringFeatureSource = new Map();
+ this.perLayer.forEach((fs, layerName) => {
+ const doShowLayer = this.mapProperties.zoom.map(
+ (z) =>
+ (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
+ [fs.layer.isDisplayed]
+ );
+ if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) {
+ /* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined)
+ *
+ * This means that we don't have to filter it, nor do we have to display it
+ *
+ * Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden.
+ * However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer!
+ * */
+ return;
+ }
+ const filtered = new FilteringFeatureSource(
+ fs.layer,
+ fs,
+ (id) => this.featureProperties.getStore(id),
+ this.layerState.globalFilters
+ );
+ filteringFeatureSource.set(layerName, filtered);
+
+ new ShowDataLayer(map, {
+ layer: fs.layer.layerDef,
+ features: filtered,
+ doShowLayer,
+ selectedElement: this.selectedElement,
+ selectedLayer: this.selectedLayer,
+ fetchStore: (id) => this.featureProperties.getStore(id)
+ });
+ });
+ return filteringFeatureSource;
+ }
+
+ /**
+ * Various small methods that need to be called
+ */
+ private miscSetup() {
+ this.userRelatedState.markLayoutAsVisited(this.layout);
+
+ this.selectedElement.addCallbackAndRunD((feature) => {
+ // As soon as we have a selected element, we clear the selected element
+ // This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature
+ // The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear
+ if (feature.properties.id === "last_click") {
+ return;
+ }
+ this.lastClickObject.features.setData([]);
+ });
+
+ if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) {
+ Utils.LoadCustomCss(this.layout.customCss);
+ }
+ }
+
+ private initHotkeys() {
+ Hotkeys.RegisterHotkey(
+ { nomod: "Escape", onUp: true },
+ Translations.t.hotkeyDocumentation.closeSidebar,
+ () => {
+ this.selectedElement.setData(undefined);
+ this.guistate.closeAll();
+ }
+ );
+
+ this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun(enable => {
+ if(!enable){
+ return
+ }
Hotkeys.RegisterHotkey(
{
- nomod: "b",
+ nomod: "b"
},
Translations.t.hotkeyDocumentation.openLayersPanel,
() => {
if (this.featureSwitches.featureSwitchFilter.data) {
- this.guistate.openFilterView()
+ this.guistate.openFilterView();
}
}
- )
-
+ );
Hotkeys.RegisterHotkey(
{ shift: "O" },
Translations.t.hotkeyDocumentation.selectMapnik,
() => {
- this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto)
+ this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto);
}
- )
+ );
const setLayerCategory = (category: EliCategory) => {
- const available = this.availableLayers.data
- const current = this.mapProperties.rasterLayer
+ const available = this.availableLayers.data;
+ const current = this.mapProperties.rasterLayer;
const best = RasterLayerUtils.SelectBestLayerAccordingTo(
available,
category,
current.data
- )
- console.log("Best layer for category", category, "is", best.properties.id)
- current.setData(best)
- }
+ );
+ console.log("Best layer for category", category, "is", best.properties.id);
+ current.setData(best);
+ };
Hotkeys.RegisterHotkey(
{ nomod: "O" },
Translations.t.hotkeyDocumentation.selectOsmbasedmap,
() => setLayerCategory("osmbasedmap")
- )
+ );
Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () =>
setLayerCategory("map")
- )
+ );
Hotkeys.RegisterHotkey(
{ nomod: "P" },
Translations.t.hotkeyDocumentation.selectAerial,
() => setLayerCategory("photo")
- )
- }
+ );
+ return true
+ })
- private addLastClick(last_click: LastClickFeatureSource) {
- // The last_click gets a _very_ special treatment as it interacts with various parts
- const last_click_layer = this.layerState.filteredLayers.get("last_click")
- this.featureProperties.trackFeatureSource(last_click)
- this.indexedFeatures.addSource(last_click)
- last_click.features.addCallbackAndRunD((features) => {
- if (this.selectedLayer.data?.id === "last_click") {
- // The last-click location moved, but we have selected the last click of the previous location
- // So, we update _after_ clearing the selection to make sure no stray data is sticking around
- this.selectedElement.setData(undefined)
- this.selectedElement.setData(features[0])
- }
- })
- new ShowDataLayer(this.map, {
- features: new FilteringFeatureSource(last_click_layer, last_click),
- doShowLayer: this.featureSwitches.featureSwitchEnableLogin,
- layer: last_click_layer.layerDef,
- selectedElement: this.selectedElement,
- selectedLayer: this.selectedLayer,
- onClick: (feature: Feature) => {
- if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) {
- this.map.data.flyTo({
- zoom: Constants.minZoomLevelToAddNewPoint,
- center: this.mapProperties.lastClickLocation.data,
- })
- return
- }
- // We first clear the selection to make sure no weird state is around
- this.selectedLayer.setData(undefined)
- this.selectedElement.setData(undefined)
+ }
- this.selectedElement.setData(feature)
- this.selectedLayer.setData(last_click_layer.layerDef)
- },
- })
- }
+ private addLastClick(last_click: LastClickFeatureSource) {
+ // The last_click gets a _very_ special treatment as it interacts with various parts
+ this.featureProperties.trackFeatureSource(last_click);
+ this.indexedFeatures.addSource(last_click);
+
+ last_click.features.addCallbackAndRunD((features) => {
+ if (this.selectedLayer.data?.id === "last_click") {
+ // The last-click location moved, but we have selected the last click of the previous location
+ // So, we update _after_ clearing the selection to make sure no stray data is sticking around
+ this.selectedElement.setData(undefined);
+ this.selectedElement.setData(features[0]);
+ }
+ });
+
+ new ShowDataLayer(this.map, {
+ features: new FilteringFeatureSource(this.newPointDialog, last_click),
+ doShowLayer: this.featureSwitches.featureSwitchEnableLogin,
+ layer: this.newPointDialog.layerDef,
+ selectedElement: this.selectedElement,
+ selectedLayer: this.selectedLayer,
+ onClick: (feature: Feature) => {
+ if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) {
+ this.map.data.flyTo({
+ zoom: Constants.minZoomLevelToAddNewPoint,
+ center: this.mapProperties.lastClickLocation.data
+ });
+ return;
+ }
+ // We first clear the selection to make sure no weird state is around
+ this.selectedLayer.setData(undefined);
+ this.selectedElement.setData(undefined);
+
+ this.selectedElement.setData(feature);
+ this.selectedLayer.setData(this.newPointDialog.layerDef);
+ }
+ });
+ }
+
+ public openNewDialog() {
+ this.selectedLayer.setData(undefined);
+ this.selectedElement.setData(undefined);
+
+ const { lon, lat } = this.mapProperties.location.data;
+ const feature = this.lastClickObject.createFeature(lon, lat)
+ this.featureProperties.trackFeature(feature)
+ this.selectedElement.setData(feature);
+ this.selectedLayer.setData(this.newPointDialog.layerDef);
+ }
+
+ /**
+ * Add the special layers to the map
+ */
+ private drawSpecialLayers() {
+ type AddedByDefaultTypes = (typeof Constants.added_by_default)[number]
+ const empty = [];
/**
- * Add the special layers to the map
+ * A listing which maps the layerId onto the featureSource
*/
- private drawSpecialLayers() {
- type AddedByDefaultTypes = (typeof Constants.added_by_default)[number]
- const empty = []
- /**
- * A listing which maps the layerId onto the featureSource
- */
- const specialLayers: Record<
- Exclude | "current_view",
- FeatureSource
- > = {
- home_location: this.userRelatedState.homeLocation,
- gps_location: this.geolocation.currentUserLocation,
- gps_location_history: this.geolocation.historicalUserLocations,
- gps_track: this.geolocation.historicalUserLocationsTrack,
- selected_element: new StaticFeatureSource(
- this.selectedElement.map((f) => (f === undefined ? empty : [f]))
- ),
- range: new StaticFeatureSource(
- this.mapProperties.maxbounds.map((bbox) =>
- bbox === undefined ? empty : [bbox.asGeoJson({ id: "range" })]
- )
- ),
- current_view: this.currentView,
- }
- if (this.layout?.lockLocation) {
- const bbox = new BBox(this.layout.lockLocation)
- this.mapProperties.maxbounds.setData(bbox)
- ShowDataLayer.showRange(
- this.map,
- new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]),
- this.featureSwitches.featureSwitchIsTesting
- )
- }
- const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view")
- if (currentViewLayer?.tagRenderings?.length > 0) {
- const params = MetaTagging.createExtraFuncParams(this)
- this.featureProperties.trackFeatureSource(specialLayers.current_view)
- specialLayers.current_view.features.addCallbackAndRunD((features) => {
- MetaTagging.addMetatags(
- features,
- params,
- currentViewLayer,
- this.layout,
- this.osmObjectDownloader,
- this.featureProperties
- )
- })
- }
-
- const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range")
-
- const rangeIsDisplayed = rangeFLayer?.isDisplayed
-
- if (
- !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef))
- ) {
- rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
- }
-
- this.layerState.filteredLayers.forEach((flayer) => {
- const id = flayer.layerDef.id
- const features: FeatureSource = specialLayers[id]
- if (features === undefined) {
- return
- }
-
- this.featureProperties.trackFeatureSource(features)
- new ShowDataLayer(this.map, {
- features,
- doShowLayer: flayer.isDisplayed,
- layer: flayer.layerDef,
- selectedElement: this.selectedElement,
- selectedLayer: this.selectedLayer,
- })
- })
- }
-
- /**
- * Setup various services for which no reference are needed
- */
- private initActors() {
- // Unselect the selected element if it is panned out of view
- this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => {
- const selected = this.selectedElement.data
- if (selected === undefined) {
- return
- }
- const bbox = BBox.get(selected)
- if (!bbox.overlapsWith(bounds)) {
- this.selectedElement.setData(undefined)
- }
- })
-
- this.selectedElement.addCallback((selected) => {
- if (selected === undefined) {
- // We did _unselect_ an item - we always remove the lastclick-object
- this.lastClickObject.features.setData([])
- this.selectedLayer.setData(undefined)
- }
- })
- new ThemeViewStateHashActor(this)
- new MetaTagging(this)
- new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this)
- new ChangeToElementsActor(this.changes, this.featureProperties)
- new PendingChangesUploader(this.changes, this.selectedElement)
- new SelectedElementTagsUpdater(this)
- new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers)
- new PreferredRasterLayerSelector(
- this.mapProperties.rasterLayer,
- this.availableLayers,
- this.featureSwitches.backgroundLayerId,
- this.userRelatedState.preferredBackgroundLayer
+ const specialLayers: Record<
+ Exclude | "current_view",
+ FeatureSource
+ > = {
+ home_location: this.userRelatedState.homeLocation,
+ gps_location: this.geolocation.currentUserLocation,
+ gps_location_history: this.geolocation.historicalUserLocations,
+ gps_track: this.geolocation.historicalUserLocationsTrack,
+ selected_element: new StaticFeatureSource(
+ this.selectedElement.map((f) => (f === undefined ? empty : [f]))
+ ),
+ range: new StaticFeatureSource(
+ this.mapProperties.maxbounds.map((bbox) =>
+ bbox === undefined ? empty : [bbox.asGeoJson({ id: "range" })]
)
+ ),
+ current_view: this.currentView
+ };
+ if (this.layout?.lockLocation) {
+ const bbox = new BBox(this.layout.lockLocation);
+ this.mapProperties.maxbounds.setData(bbox);
+ ShowDataLayer.showRange(
+ this.map,
+ new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]),
+ this.featureSwitches.featureSwitchIsTesting
+ );
}
+ const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view");
+ if (currentViewLayer?.tagRenderings?.length > 0) {
+ const params = MetaTagging.createExtraFuncParams(this);
+ this.featureProperties.trackFeatureSource(specialLayers.current_view);
+ specialLayers.current_view.features.addCallbackAndRunD((features) => {
+ MetaTagging.addMetatags(
+ features,
+ params,
+ currentViewLayer,
+ this.layout,
+ this.osmObjectDownloader,
+ this.featureProperties
+ );
+ });
+ }
+
+ const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range");
+
+ const rangeIsDisplayed = rangeFLayer?.isDisplayed;
+
+ if (
+ !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef))
+ ) {
+ rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true);
+ }
+
+ this.layerState.filteredLayers.forEach((flayer) => {
+ const id = flayer.layerDef.id;
+ const features: FeatureSource = specialLayers[id];
+ if (features === undefined) {
+ return;
+ }
+
+ this.featureProperties.trackFeatureSource(features);
+ new ShowDataLayer(this.map, {
+ features,
+ doShowLayer: flayer.isDisplayed,
+ layer: flayer.layerDef,
+ selectedElement: this.selectedElement,
+ selectedLayer: this.selectedLayer
+ });
+ });
+ }
+
+ /**
+ * Setup various services for which no reference are needed
+ */
+ private initActors() {
+ // Unselect the selected element if it is panned out of view
+ this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => {
+ const selected = this.selectedElement.data;
+ if (selected === undefined) {
+ return;
+ }
+ const bbox = BBox.get(selected);
+ if (!bbox.overlapsWith(bounds)) {
+ this.selectedElement.setData(undefined);
+ }
+ });
+
+ this.selectedElement.addCallback((selected) => {
+ if (selected === undefined) {
+ // We did _unselect_ an item - we always remove the lastclick-object
+ this.lastClickObject.features.setData([]);
+ this.selectedLayer.setData(undefined);
+ }
+ });
+ new ThemeViewStateHashActor(this);
+ new MetaTagging(this);
+ new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this);
+ new ChangeToElementsActor(this.changes, this.featureProperties);
+ new PendingChangesUploader(this.changes, this.selectedElement);
+ new SelectedElementTagsUpdater(this);
+ new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers);
+ new PreferredRasterLayerSelector(
+ this.mapProperties.rasterLayer,
+ this.availableLayers,
+ this.featureSwitches.backgroundLayerId,
+ this.userRelatedState.preferredBackgroundLayer
+ );
+ }
}
diff --git a/src/UI/Base/FileSelector.svelte b/src/UI/Base/FileSelector.svelte
index 3a63f8653..9bd5b3f8e 100644
--- a/src/UI/Base/FileSelector.svelte
+++ b/src/UI/Base/FileSelector.svelte
@@ -12,7 +12,28 @@
let id = Math.random() * 1000000000 + ""
-
diff --git a/src/UI/Base/FloatOver.svelte b/src/UI/Base/FloatOver.svelte
index 701b19b39..b86baf78d 100644
--- a/src/UI/Base/FloatOver.svelte
+++ b/src/UI/Base/FloatOver.svelte
@@ -11,8 +11,9 @@
{dispatch("close")}}
>
-
+
{}}>
diff --git a/src/UI/Base/Hotkeys.ts b/src/UI/Base/Hotkeys.ts
index 6b4f07365..e40d92166 100644
--- a/src/UI/Base/Hotkeys.ts
+++ b/src/UI/Base/Hotkeys.ts
@@ -22,7 +22,14 @@ export default class Hotkeys {
}[]
>([])
- private static textElementSelected(): boolean {
+ private static textElementSelected(event: KeyboardEvent): boolean {
+ if(event.ctrlKey || event.altKey){
+ // This is an event with a modifier-key, lets not ignore it
+ return false
+ }
+ if(event.key === "Escape"){
+ return false // Another not-printable character that should not be ignored
+ }
return ["input", "textarea"].includes(document?.activeElement?.tagName?.toLowerCase())
}
public static RegisterHotkey(
@@ -68,7 +75,7 @@ export default class Hotkeys {
})
} else if (key["shift"] !== undefined) {
document.addEventListener(type, function (event) {
- if (Hotkeys.textElementSelected()) {
+ if (Hotkeys.textElementSelected(event)) {
// A text element is selected, we don't do anything special
return
}
@@ -86,7 +93,7 @@ export default class Hotkeys {
})
} else if (key["nomod"] !== undefined) {
document.addEventListener(type, function (event) {
- if (Hotkeys.textElementSelected()) {
+ if (Hotkeys.textElementSelected(event)) {
// A text element is selected, we don't do anything special
return
}
diff --git a/src/UI/Base/TabbedGroup.svelte b/src/UI/Base/TabbedGroup.svelte
index 7e33100e7..35d9a865b 100644
--- a/src/UI/Base/TabbedGroup.svelte
+++ b/src/UI/Base/TabbedGroup.svelte
@@ -1,20 +1,32 @@