",
+ "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 9532bd0cf2..29acc68352 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 90f4dc383b..f34d3e69c4 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 0000000000..30a857b7be
--- /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 488a88fe3c..bcc2c2d96b 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 38544e0b7b..047ade494c 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 650095516c..d9eaa8963d 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 fa1aba2570..be71fd48a6 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 799ac2d4c9..0a10f413ab 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 f2d2af6650..3f98526768 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 b91b7df7a7..739fbd9400 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 70101f0b20..03f4923e48 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 ef52f608cf..4f5b4e4deb 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 6649d61cab..b530e79eac 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 c938e07dc2..e7e8d676fd 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 f6fedc58e6..139a801862 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 8ab1faecff..ece8d156de 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 3fda13afd6..a12835fbb1 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 b10208f09e..8527d8d796 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 ad68f6979e..39aa182ad7 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 700a0cb300..9ef7f9250a 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 f9b3c2d5f3..d399ed5ae4 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 a15f6b0569..f52da26f50 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 3a63f86539..9bd5b3f8e6 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 701b19b397..b86baf78d3 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 6b4f073650..e40d921664 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 7e33100e70..35d9a865b1 100644
--- a/src/UI/Base/TabbedGroup.svelte
+++ b/src/UI/Base/TabbedGroup.svelte
@@ -1,20 +1,32 @@