From 2736740cd0d777450f8da7eea76f1defa7993944 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 8 Oct 2023 14:09:29 +0200 Subject: [PATCH 01/19] Fix: add all vector layers to CSP --- scripts/generateLayouts.ts | 3 +-- src/Models/RasterLayers.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index c938e07dc..e45c332bf 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -245,8 +245,7 @@ function generateCsp( ...Constants.defaultOverpassUrls, Constants.countryCoderEndpoint, Constants.nominatimEndpoint, - AvailableRasterLayers.maptilerCarto.properties.url, - AvailableRasterLayers.maptilerDefaultLayer.properties.url, + ...AvailableRasterLayers.vectorLayers.map(l => l.properties.url), "https://api.openstreetmap.org", "https://pietervdvn.goatcounter.com", ] diff --git a/src/Models/RasterLayers.ts b/src/Models/RasterLayers.ts index 700a0cb30..c21c0ca25 100644 --- a/src/Models/RasterLayers.ts +++ b/src/Models/RasterLayers.ts @@ -100,6 +100,12 @@ export class AvailableRasterLayers { 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 { @@ -122,13 +128,7 @@ export class AvailableRasterLayers { return GeoOperations.inside(lonlat, eliPolygon) }) matching.push(...AvailableRasterLayers.globalLayers) - matching.unshift( - AvailableRasterLayers.maptilerDefaultLayer, - AvailableRasterLayers.osmCarto, - AvailableRasterLayers.maptilerCarto, - AvailableRasterLayers.maptilerBackdrop, - AvailableRasterLayers.americana - ) + matching.unshift(...AvailableRasterLayers.vectorLayers) return matching }) ) From 93630bd1db9e71f5e62619b2d02aaf18b42222f6 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 8 Oct 2023 17:38:28 +0200 Subject: [PATCH 02/19] Fix: add all global layers to CSP --- scripts/generateLayouts.ts | 43 ++- src/Models/RasterLayers.ts | 630 +++++++++++++++++++------------------ 2 files changed, 336 insertions(+), 337 deletions(-) diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index e45c332bf..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,7 +243,6 @@ function generateCsp( ...Constants.defaultOverpassUrls, Constants.countryCoderEndpoint, Constants.nominatimEndpoint, - ...AvailableRasterLayers.vectorLayers.map(l => l.properties.url), "https://api.openstreetmap.org", "https://pietervdvn.goatcounter.com", ] diff --git a/src/Models/RasterLayers.ts b/src/Models/RasterLayers.ts index c21c0ca25..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 readonly vectorLayers = [ AvailableRasterLayers.maptilerDefaultLayer, - AvailableRasterLayers.osmCarto, - AvailableRasterLayers.maptilerCarto, - AvailableRasterLayers.maptilerBackdrop, - AvailableRasterLayers.americana] + 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 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 - } + 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; } From 6f376291cec7f2f168b27b657d7f1aac25ca825f Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 6 Oct 2023 14:40:50 +0200 Subject: [PATCH 03/19] Dev: show IP-address instead of when booted (somewhat of a hack) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c843af373..e929de00a 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 index.css -o public/css/index-tailwind-output.css --watch", "generate:css": "tailwindcss -i src/index.css -o public/css/index-tailwind-output.css", From 650c1a675c783179482338f449ca1202246916a0 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 6 Oct 2023 14:41:22 +0200 Subject: [PATCH 04/19] Fix: fix updating of styles --- src/UI/Map/ShowDataLayer.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/UI/Map/ShowDataLayer.ts b/src/UI/Map/ShowDataLayer.ts index 671b6478d..34ac01078 100644 --- a/src/UI/Map/ShowDataLayer.ts +++ b/src/UI/Map/ShowDataLayer.ts @@ -16,6 +16,7 @@ import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" import FilteredLayer from "../../Models/FilteredLayer" import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource" +import { CLIENT_RENEG_LIMIT } from "tls"; class PointRenderingLayer { private readonly _config: PointRenderingConfig @@ -229,7 +230,10 @@ class LineRenderingLayer { const self = this features.features.addCallbackAndRunD(() => self.update(features.features)) - map.on("styledata", () => self.update(features.features)) + map.on("styledata", () => { + self._listenerInstalledOn.clear() + return self.update(features.features); + }) } public destruct(): void { @@ -406,13 +410,10 @@ class LineRenderingLayer { } else { const tags = this._fetchStore(id) this._listenerInstalledOn.add(id) - map.setFeatureState( - { source: this._layername, id }, - this.calculatePropsFor(feature.properties) - ) - tags.addCallbackD((properties) => { - if (!map.getLayer(this._layername)) { - return + tags.addCallbackAndRunD((properties) => { + // Make sure to use 'getSource' here, the layer names are different! + if(map.getSource(this._layername) === undefined){ + return true } map.setFeatureState( { source: this._layername, id }, From 46e7cf58333eda32946b0605fc9c1631a6af51d9 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 6 Oct 2023 15:14:51 +0200 Subject: [PATCH 05/19] Some tweaking --- src/UI/Map/MapLibreAdaptor.ts | 22 +++++++++++----------- src/UI/Map/ShowDataLayer.ts | 5 +---- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/UI/Map/MapLibreAdaptor.ts b/src/UI/Map/MapLibreAdaptor.ts index d41bcc25d..a94a32f6d 100644 --- a/src/UI/Map/MapLibreAdaptor.ts +++ b/src/UI/Map/MapLibreAdaptor.ts @@ -1,14 +1,14 @@ -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import type { Map as MLMap } from "maplibre-gl" -import { Map as MlMap, SourceSpecification } from "maplibre-gl" -import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers" -import { Utils } from "../../Utils" -import { BBox } from "../../Logic/BBox" -import { ExportableMap, MapProperties } from "../../Models/MapProperties" -import SvelteUIElement from "../Base/SvelteUIElement" -import MaplibreMap from "./MaplibreMap.svelte" -import { RasterLayerProperties } from "../../Models/RasterLayerProperties" -import * as htmltoimage from "html-to-image" +import { Store, UIEventSource } from "../../Logic/UIEventSource"; +import type { Map as MLMap } from "maplibre-gl"; +import { Map as MlMap, SourceSpecification } from "maplibre-gl"; +import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"; +import { Utils } from "../../Utils"; +import { BBox } from "../../Logic/BBox"; +import { ExportableMap, MapProperties } from "../../Models/MapProperties"; +import SvelteUIElement from "../Base/SvelteUIElement"; +import MaplibreMap from "./MaplibreMap.svelte"; +import { RasterLayerProperties } from "../../Models/RasterLayerProperties"; +import * as htmltoimage from "html-to-image"; /** * The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties` diff --git a/src/UI/Map/ShowDataLayer.ts b/src/UI/Map/ShowDataLayer.ts index 34ac01078..121f00719 100644 --- a/src/UI/Map/ShowDataLayer.ts +++ b/src/UI/Map/ShowDataLayer.ts @@ -230,10 +230,7 @@ class LineRenderingLayer { const self = this features.features.addCallbackAndRunD(() => self.update(features.features)) - map.on("styledata", () => { - self._listenerInstalledOn.clear() - return self.update(features.features); - }) + map.on("styledata", () => self.update(features.features)) } public destruct(): void { From 52e647669482f676a94730e7669a84130bf3cfc7 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 9 Oct 2023 00:43:11 +0200 Subject: [PATCH 06/19] Feature: add 'filter'-button shortcut to bottom-left controls --- src/UI/ThemeViewGUI.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index a20628a3a..dff70dc7e 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -52,6 +52,7 @@ import LanguagePicker from "./LanguagePicker" import Locale from "./i18n/Locale" import ShareScreen from "./BigComponents/ShareScreen.svelte" + import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"; export let state: ThemeViewState let layout = state.layout @@ -170,6 +171,10 @@
+ state.guistate.openFilterView()}> + + + Date: Mon, 9 Oct 2023 00:52:06 +0200 Subject: [PATCH 07/19] Don't ignore escape if a textfield is selected --- src/UI/Base/Hotkeys.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 } From 285fe9ab833b0cf0c0ea00a011457a83bd7ad6d2 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 9 Oct 2023 01:26:12 +0200 Subject: [PATCH 08/19] UX: place 'add new button' in bottom-left instead of having a dynamic pin --- langs/en.json | 2 +- langs/nl.json | 2 +- .../Sources/LastClickFeatureSource.ts | 68 +- src/Logic/Web/ThemeViewStateHashActor.ts | 2 +- src/Models/Constants.ts | 2 +- src/Models/ThemeViewState.ts | 1164 +++++++++-------- src/UI/ThemeViewGUI.svelte | 193 +-- 7 files changed, 730 insertions(+), 703 deletions(-) diff --git a/langs/en.json b/langs/en.json index 7167afb95..7bd2338e5 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/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts index ecd8d8288..b45169c0a 100644 --- a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts @@ -1,10 +1,11 @@ -import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" -import { WritableFeatureSource } from "../FeatureSource" -import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" -import { Feature, Point } from "geojson" -import { TagUtils } from "../../Tags/TagUtils" -import BaseUIElement from "../../../UI/BaseUIElement" -import { Utils } from "../../../Utils" +import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; +import { WritableFeatureSource } from "../FeatureSource"; +import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"; +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([]) + private i: number = 0 + private readonly hasNoteLayer: string + private readonly renderings: string[]; + private readonly hasPresets: string; constructor(location: Store<{ lon: number; lat: number }>, layout: LayoutConfig) { + this.hasNoteLayer = layout.layers.some((l) => l.id === "note") ? "yes" : "no" + this.hasPresets= layout.layers.some((l) => l.presets?.length > 0) ? "yes" : "no" 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 , + has_presets:this.hasPresets , + 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/Web/ThemeViewStateHashActor.ts b/src/Logic/Web/ThemeViewStateHashActor.ts index b10208f09..93a2326e9 100644 --- a/src/Logic/Web/ThemeViewStateHashActor.ts +++ b/src/Logic/Web/ThemeViewStateHashActor.ts @@ -175,7 +175,7 @@ export default class ThemeViewStateHashActor { } private back() { - console.log("Got a back event") + console.trace("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/ThemeViewState.ts b/src/Models/ThemeViewState.ts index a15f6b056..6a03a41d9 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,573 @@ 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.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting; + this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin; + + 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.selectedElementAndLayer = this.selectedElement.mapD( + (feature) => { + const layer = this.selectedLayer.data; + if (!layer) { + return undefined; + } + return { layer, feature }; + }, + [this.selectedLayer] + ); + + 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; + } + + { + /* 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 + */ + + if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) { + this.fullNodeDatabase = new FullNodeDatabaseSource(); + } + + const layoutSource = new LayoutSource( + layout.layers, + this.featureSwitches, + this.mapProperties, + this.osmConnection.Backend(), + (id) => self.layerState.filteredLayers.get(id).isDisplayed, + this.fullNodeDatabase + ); + + 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 + }) + ]; }) - 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.selectedElementAndLayer = this.selectedElement.mapD( - (feature) => { - const layer = this.selectedLayer.data - if (!layer) { - return undefined - } - return { layer, feature } - }, - [this.selectedLayer] - ) - - 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) + ); + this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds); + this.dataIsLoading = layoutSource.isLoading; + const indexedElements = this.indexedFeatures; + this.featureProperties = new FeaturePropertiesStore(indexedElements); + this.changes = new Changes( { - 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 - } + 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); + const perLayer = new PerLayerFeatureSourceSplitter( + Array.from(this.layerState.filteredLayers.values()).filter( + (l) => l.layerDef?.source !== null + ), + new ChangeGeometryApplicator(this.indexedFeatures, this.changes), { - /* 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 - */ - - if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) { - this.fullNodeDatabase = new FullNodeDatabaseSource() - } - - const layoutSource = new LayoutSource( - layout.layers, - this.featureSwitches, - this.mapProperties, - this.osmConnection.Backend(), - (id) => self.layerState.filteredLayers.get(id).isDisplayed, - this.fullNodeDatabase - ) - - this.indexedFeatures = layoutSource - - 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 - - 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) - - 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 + constructStore: (features, layer) => + new GeoIndexedStoreForLayer(features, layer), + handleLeftovers: (features) => { + console.warn( + "Got ", + features.length, + "leftover features, such as", + features[0].properties + ); + } } - this.perLayer.forEach((fs) => { - new SaveFeatureSourceToLocalStorage( - this.osmConnection.Backend(), - fs.layer.layerDef.id, - 15, - fs, - this.featureProperties, - fs.layer.layerDef.maxAgeOfCache - ) - }) + ); + 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"); - 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.addLastClick(lastClick) - this.drawSpecialLayers() - this.initHotkeys() - this.miscSetup() - if (!Utils.runningFromConsole) { - console.log("State setup completed", this) + 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(); + // TODO remove this.addLastClick(lastClick); + this.drawSpecialLayers(); + this.initHotkeys(); + this.miscSetup(); + if (!Utils.runningFromConsole) { + console.log("State setup completed", this); } + } - 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] - ) + 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) + 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 + 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(); + } + ); + + Hotkeys.RegisterHotkey( + { + nomod: "b" + }, + Translations.t.hotkeyDocumentation.openLayersPanel, + () => { + if (this.featureSwitches.featureSwitchFilter.data) { + this.guistate.openFilterView(); + } + } + ); + + Hotkeys.RegisterHotkey( + { shift: "O" }, + Translations.t.hotkeyDocumentation.selectMapnik, + () => { + this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto); + } + ); + const setLayerCategory = (category: EliCategory) => { + 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); + }; + + 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") + ); + } + + 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 = []; /** - * Various small methods that need to be called + * A listing which maps the layerId onto the featureSource */ - 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) - } + 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 + ); + }); } - private initHotkeys() { - Hotkeys.RegisterHotkey( - { nomod: "Escape", onUp: true }, - Translations.t.hotkeyDocumentation.closeSidebar, - () => { - this.selectedElement.setData(undefined) - this.guistate.closeAll() - } - ) + const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range"); - Hotkeys.RegisterHotkey( - { - nomod: "b", - }, - Translations.t.hotkeyDocumentation.openLayersPanel, - () => { - if (this.featureSwitches.featureSwitchFilter.data) { - this.guistate.openFilterView() - } - } - ) + const rangeIsDisplayed = rangeFLayer?.isDisplayed; - Hotkeys.RegisterHotkey( - { shift: "O" }, - Translations.t.hotkeyDocumentation.selectMapnik, - () => { - this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto) - } - ) - const setLayerCategory = (category: EliCategory) => { - 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) - } - - 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") - ) + if ( + !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef)) + ) { + rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true); } - private addLastClick(last_click: LastClickFeatureSource) { - // The last_click gets a _very_ special treatment as it interacts with various parts + this.layerState.filteredLayers.forEach((flayer) => { + const id = flayer.layerDef.id; + const features: FeatureSource = specialLayers[id]; + if (features === undefined) { + return; + } - const last_click_layer = this.layerState.filteredLayers.get("last_click") - this.featureProperties.trackFeatureSource(last_click) - this.indexedFeatures.addSource(last_click) + this.featureProperties.trackFeatureSource(features); + new ShowDataLayer(this.map, { + features, + doShowLayer: flayer.isDisplayed, + layer: flayer.layerDef, + selectedElement: this.selectedElement, + selectedLayer: this.selectedLayer + }); + }); + } - 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]) - } - }) + /** + * 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); + } + }); - 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) - }, - }) - } - - /** - * Add the special layers to the map - */ - 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 - ) - } + 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/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index dff70dc7e..43f86cda0 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -1,115 +1,114 @@
@@ -169,22 +168,30 @@
-
- - state.guistate.openFilterView()}> - - +
@@ -319,7 +326,7 @@ new CopyrightPanel(state)} slot="content3" /> -
+
@@ -347,7 +354,9 @@ - + { + selectedElement.setData(undefined) + }}>
From 5be24dbef1647cd2c6c869d86ea5b37f0e072106 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 9 Oct 2023 01:53:27 +0200 Subject: [PATCH 09/19] Fix: drag & drop for file selector --- src/UI/Base/FileSelector.svelte | 44 +++++++++++++++++---------------- 1 file changed, 23 insertions(+), 21 deletions(-) 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 + "" -
+ { + drawAttention = false + dispatcher("submit", inputElement.files) + }} + on:dragend={() => { + console.log("Drag end") + drawAttention = false + }} + on:dragenter|preventDefault|stopPropagation={(e) => { + console.log("Dragging enter") + drawAttention = true + e.dataTransfer.drop = "copy" + }} + on:dragstart={() => { + console.log("DragStart") + drawAttention = false + }} + on:drop|preventDefault|stopPropagation={(e) => { + console.log("Got a 'drop'") + drawAttention = false + dispatcher("submit", e.dataTransfer.files) + }}> @@ -23,26 +44,7 @@ id={"fileinput" + id} {multiple} name="file-input" - on:change|preventDefault={() => { - drawAttention = false - dispatcher("submit", inputElement.files) - }} - on:dragend={() => { - drawAttention = false - }} - on:dragover|preventDefault|stopPropagation={(e) => { - console.log("Dragging over!") - drawAttention = true - e.dataTransfer.drop = "copy" - }} - on:dragstart={() => { - drawAttention = false - }} - on:drop|preventDefault|stopPropagation={(e) => { - console.log("Got a 'drop'") - drawAttention = false - dispatcher("submit", e.dataTransfer.files) - }} + type="file" />
From d43f8c00800e38ea5c0cca8484ac7586e67cf368 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 9 Oct 2023 02:07:25 +0200 Subject: [PATCH 10/19] Feature: close floatover if pressed outside of it, fix #1647 --- src/Logic/Web/ThemeViewStateHashActor.ts | 1 - src/UI/Base/FloatOver.svelte | 3 ++- src/UI/ThemeViewGUI.svelte | 8 +++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Logic/Web/ThemeViewStateHashActor.ts b/src/Logic/Web/ThemeViewStateHashActor.ts index 93a2326e9..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.trace("Got a back event") const state = this._state // history.pushState(null, null, window.location.pathname); if (state.selectedElement.data) { 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/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 43f86cda0..5bbbed5a3 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -267,7 +267,7 @@ - + state.guistate.themeIsOpened.setData(false)}>
@@ -339,7 +339,7 @@ - state.guistate.backgroundLayerSelectionIsOpened.setData(false)}> + {state.guistate.backgroundLayerSelectionIsOpened.setData(false)}}>
- { - selectedElement.setData(undefined) - }}> + state.guistate.menuIsOpened.setData(false) }>
From bde5878fedae0eac20db8bb4c6eca81f904e20fd Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 9 Oct 2023 02:52:22 +0200 Subject: [PATCH 11/19] Fix: correct minzoom on all themes --- .../layers/ticket_machine/ticket_machine.json | 2 +- .../ticket_validator/ticket_validator.json | 2 +- assets/themes/bag/bag.json | 10 +++---- assets/themes/blind_osm/blind_osm.json | 4 +-- assets/themes/climbing/climbing.json | 5 +++- assets/themes/healthcare/healthcare.json | 4 +-- assets/themes/onwheels/onwheels.json | 16 ++++++------ assets/themes/pets/pets.json | 6 ++--- assets/themes/stations/stations.json | 26 +++++++++---------- .../street_lighting/street_lighting.json | 4 +-- assets/themes/transit/transit.json | 11 +++----- 11 files changed, 45 insertions(+), 45 deletions(-) diff --git a/assets/layers/ticket_machine/ticket_machine.json b/assets/layers/ticket_machine/ticket_machine.json index 6292fc6f9..8f60ae234 100644 --- a/assets/layers/ticket_machine/ticket_machine.json +++ b/assets/layers/ticket_machine/ticket_machine.json @@ -20,7 +20,7 @@ ] } }, - "minzoom": 19, + "minzoom": 18, "title": { "render": { "en": "Ticket Machine", diff --git a/assets/layers/ticket_validator/ticket_validator.json b/assets/layers/ticket_validator/ticket_validator.json index b0b93552a..16520ec3a 100644 --- a/assets/layers/ticket_validator/ticket_validator.json +++ b/assets/layers/ticket_validator/ticket_validator.json @@ -13,7 +13,7 @@ "source": { "osmTags": "amenity=ticket_validator" }, - "minzoom": 19, + "minzoom": 18, "title": { "render": { "en": "Ticket Validator", diff --git a/assets/themes/bag/bag.json b/assets/themes/bag/bag.json index 8f33320cb..204226ed3 100644 --- a/assets/themes/bag/bag.json +++ b/assets/themes/bag/bag.json @@ -47,7 +47,7 @@ "osmTags": "building~*", "maxCacheAge": 0 }, - "minzoom": 19, + "minzoom": 18, "calculatedTags": [ "_surface:strict:=feat(get)('_surface')" ], @@ -154,7 +154,7 @@ }, "maxCacheAge": 0 }, - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "label": { @@ -194,7 +194,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 */ )", @@ -379,7 +379,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}`", @@ -427,4 +427,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/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 8b242d1af..c358d0a3b 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 } }, { @@ -130,7 +130,7 @@ ] }, "presets": null, - "minzoom": 19 + "minzoom": 18 } }, { @@ -143,7 +143,7 @@ ] }, "presets": null, - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "circle:white;./assets/themes/stations/bicycle_parking.svg" @@ -161,7 +161,7 @@ ] }, "presets": null, - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "circle:white;./assets/themes/stations/rental_bicycle.svg" @@ -179,7 +179,7 @@ ] }, "presets": null, - "minzoom": 19 + "minzoom": 18 } }, { @@ -195,7 +195,7 @@ ] }, "presets": null, - "minzoom": 19, + "minzoom": 18, "mapRendering+": [ { "color": "#00f", @@ -214,7 +214,7 @@ ] }, "presets": null, - "minzoom": 19, + "minzoom": 18, "mapRendering+": [ { "color": "yellow", @@ -235,13 +235,13 @@ "clock" ], "override": { - "minzoom": 19 + "minzoom": 18 } }, { "builtin": "bench", "override": { - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "./assets/themes/stations/bench.svg" @@ -252,7 +252,7 @@ { "builtin": "drinking_water", "override": { - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "circle:white;./assets/themes/stations/drinking_water.svg" @@ -293,7 +293,7 @@ "zh_Hant": "時刻表" } }, - "minzoom": 19, + "minzoom": 18, "source": { "osmTags": { "and": [ @@ -412,4 +412,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 0ba9c2ab4..7d1c8da7f 100644 --- a/assets/themes/street_lighting/street_lighting.json +++ b/assets/themes/street_lighting/street_lighting.json @@ -221,7 +221,7 @@ ] } }, - "minzoom": 19, + "minzoom": 18, "title": { "render": { "en": "Street", @@ -361,4 +361,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 +} From 66c69602af517c7f37d2cc06e02b4672878ae6dc Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 9 Oct 2023 02:53:30 +0200 Subject: [PATCH 12/19] UI: align some buttons, fix #1651 --- src/UI/Popup/DeleteFlow/DeleteWizard.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UI/Popup/DeleteFlow/DeleteWizard.svelte b/src/UI/Popup/DeleteFlow/DeleteWizard.svelte index 66781eabb..a92e4de38 100644 --- a/src/UI/Popup/DeleteFlow/DeleteWizard.svelte +++ b/src/UI/Popup/DeleteFlow/DeleteWizard.svelte @@ -92,7 +92,7 @@ {#if currentState === "start"} - Date: Tue, 10 Oct 2023 13:27:56 +0200 Subject: [PATCH 13/19] Themes: add guidepost theme --- assets/themes/guideposts/guideposts.json | 14 ++++++++++++++ src/Models/ThemeConfig/LayoutConfig.ts | 3 +++ 2 files changed, 17 insertions(+) create mode 100644 assets/themes/guideposts/guideposts.json 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/src/Models/ThemeConfig/LayoutConfig.ts b/src/Models/ThemeConfig/LayoutConfig.ts index 9781734dd..3a88dc569 100644 --- a/src/Models/ThemeConfig/LayoutConfig.ts +++ b/src/Models/ThemeConfig/LayoutConfig.ts @@ -94,6 +94,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) From 02da68a62e24e02d7e3a9ce21b62445f511f080b Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 00:35:34 +0200 Subject: [PATCH 14/19] CI: add studio to caddyfile --- scripts/hetzner/config/Caddyfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/hetzner/config/Caddyfile b/scripts/hetzner/config/Caddyfile index 27b328008..139a80186 100644 --- a/scripts/hetzner/config/Caddyfile +++ b/scripts/hetzner/config/Caddyfile @@ -22,3 +22,9 @@ report.mapcomplete.org { to http://127.0.0.1:2600 } } + +studio.mapcomplete.org { + reverse_proxy { + to http://127.0.0.1:1235 + } +} From 17503d5bfb63c5c03968c96f15c2c5a674a3ede3 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 01:41:42 +0200 Subject: [PATCH 15/19] Fix: hide some elements of the UI if they are disabled by a featureSwitch --- .../Sources/LastClickFeatureSource.ts | 50 ++-- src/Logic/State/FeatureSwitchState.ts | 2 +- src/Models/ThemeViewState.ts | 92 +++--- src/UI/Base/TabbedGroup.svelte | 180 ++++++------ src/UI/BigComponents/ThemeIntroPanel.svelte | 183 ++++++------ src/UI/ThemeViewGUI.svelte | 261 +++++++++--------- 6 files changed, 402 insertions(+), 366 deletions(-) diff --git a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts index b45169c0a..40705891f 100644 --- a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts @@ -12,57 +12,57 @@ import { OsmTags } from "../../../Models/OsmFeature"; * Based on a lon/lat UIEVentSource, will generate the corresponding feature with the correct properties */ export class LastClickFeatureSource implements WritableFeatureSource { - public readonly features: UIEventSource = new UIEventSource([]) - private i: number = 0 - private readonly hasNoteLayer: string - private readonly renderings: string[]; - private readonly hasPresets: string; + 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") ? "yes" : "no" - this.hasPresets= layout.layers.some((l) => l.presets?.length > 0) ? "yes" : "no" - const allPresets: BaseUIElement[] = [] + 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++) { - const preset = layer.presets[i] - const tags = new ImmutableStore(TagUtils.KVtoProperties(preset.tags)) + const preset = layer.presets[i]; + const tags = new ImmutableStore(TagUtils.KVtoProperties(preset.tags)); const { html } = layer.mapRendering[0].RenderIcon(tags, false, { noSize: true, - includeBadges: false, - }) - allPresets.push(html) + includeBadges: false + }); + allPresets.push(html); } this.renderings = Utils.Dedup( allPresets.map((uiElem) => Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML ) - ) + ); location.addCallbackAndRunD(({ lon, lat }) => { - this.features.setData([this.createFeature(lon, lat)]) - }) + 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 , - has_presets:this.hasPresets , + 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++ + number_of_presets: "" + this.renderings.length, + first_preset: this.renderings[0] + }; + this.i++; return >{ type: "Feature", properties, geometry: { type: "Point", - coordinates: [lon, lat], - }, - } + 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/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 6a03a41d9..f52da26f5 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -338,7 +338,6 @@ export default class ThemeViewState implements SpecialVisualizationState { ); this.initActors(); - // TODO remove this.addLastClick(lastClick); this.drawSpecialLayers(); this.initHotkeys(); this.miscSetup(); @@ -417,52 +416,61 @@ export default class ThemeViewState implements SpecialVisualizationState { } ); - Hotkeys.RegisterHotkey( - { - nomod: "b" - }, - Translations.t.hotkeyDocumentation.openLayersPanel, - () => { - if (this.featureSwitches.featureSwitchFilter.data) { - this.guistate.openFilterView(); + this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun(enable => { + if(!enable){ + return } - } - ); + Hotkeys.RegisterHotkey( + { + nomod: "b" + }, + Translations.t.hotkeyDocumentation.openLayersPanel, + () => { + if (this.featureSwitches.featureSwitchFilter.data) { + this.guistate.openFilterView(); + } + } + ); + Hotkeys.RegisterHotkey( + { shift: "O" }, + Translations.t.hotkeyDocumentation.selectMapnik, + () => { + this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto); + } + ); + const setLayerCategory = (category: EliCategory) => { + 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); + }; + + 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 + }) - Hotkeys.RegisterHotkey( - { shift: "O" }, - Translations.t.hotkeyDocumentation.selectMapnik, - () => { - this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto); - } - ); - const setLayerCategory = (category: EliCategory) => { - 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); - }; - 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") - ); } private addLastClick(last_click: LastClickFeatureSource) { 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 @@
@@ -29,41 +41,31 @@ >
- {#if $$slots.title1} - twJoin("tab", selected && "primary")}> -
- Tab 0 -
-
- {/if} - {#if $$slots.title1} - twJoin("tab", selected && "primary")}> -
- -
-
- {/if} - {#if $$slots.title2} - twJoin("tab", selected && "primary")}> -
- -
-
- {/if} - {#if $$slots.title3} - twJoin("tab", selected && "primary")}> -
- -
-
- {/if} - {#if $$slots.title4} - twJoin("tab", selected && "primary")}> -
- -
-
- {/if} + twJoin("tab", selected && "primary", !$condition0 && "hidden")}> +
+ Tab 0 +
+
+ twJoin("tab", selected && "primary", !$condition1 && "hidden")}> +
+ +
+
+ twJoin("tab", selected && "primary", !$condition2 && "hidden")}> +
+ +
+
+ twJoin("tab", selected && "primary", !$condition3 && "hidden")}> +
+ +
+
+ twJoin("tab", selected && "primary", !$condition4 && "hidden")}> +
+ +
+
@@ -75,16 +77,24 @@ - + +
+ - + +
+ - + +
+ - + +
+
@@ -92,44 +102,44 @@
diff --git a/src/UI/BigComponents/ThemeIntroPanel.svelte b/src/UI/BigComponents/ThemeIntroPanel.svelte index a109862d5..a65c28906 100644 --- a/src/UI/BigComponents/ThemeIntroPanel.svelte +++ b/src/UI/BigComponents/ThemeIntroPanel.svelte @@ -1,47 +1,48 @@
@@ -62,61 +63,67 @@
-
- {#if $currentGPSLocation !== undefined || $geopermission === "prompt"} - - - {:else if $geopermission === "requested"} - - {:else if $geopermission === "denied"} - - {:else} - - {/if} -
-
- state.guistate.themeIsOpened.setData(false)} - on:searchIsValid={(isValid) => { +
+ + {#if $currentGPSLocation !== undefined || $geopermission === "prompt"} + + + {:else if $geopermission === "requested"} + + {:else if $geopermission === "denied"} + + {:else} + + {/if} + + + + +
+
+ state.guistate.themeIsOpened.setData(false)} + on:searchIsValid={(isValid) => { searchEnabled = isValid }} - perLayer={state.perLayer} - {selectedElement} - {selectedLayer} - {triggerSearch} - /> + perLayer={state.perLayer} + {selectedElement} + {selectedLayer} + {triggerSearch} + /> +
+
- -
+
diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 5bbbed5a3..878b4ee16 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -1,114 +1,114 @@
@@ -169,19 +169,28 @@
- - - - + + {#if state.lastClickObject.hasPresets || state.lastClickObject.hasNoteLayer} + + {/if} + +
- state.guistate.openFilterView()}> - - - - + + state.guistate.openFilterView()}> + + + + + + { @@ -267,9 +276,9 @@ - state.guistate.themeIsOpened.setData(false)}> + state.guistate.themeIsOpened.setData(false)}> - +
- - - - + +
@@ -310,6 +317,7 @@ /> {/each}
+
@@ -356,7 +364,8 @@ state.guistate.menuIsOpened.setData(false) }> - +
-
@@ -442,12 +450,15 @@
- - + + + new OpenJosm(state.osmConnection, state.mapProperties.bounds).SetClass("w-full")} - /> - + /> + + +
From 33565ff4c15d50e07692cb93b0476892d5284077 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 01:53:39 +0200 Subject: [PATCH 16/19] CI: attempt to fix translation resources --- scripts/build.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 6649d61ca..84330572f 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -48,11 +48,10 @@ 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/ +ls dist/assets/langs/layers/ export NODE_OPTIONS="" From 3aa49b86979e71c4776562b0b25d3b6e314d9c37 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 03:03:14 +0200 Subject: [PATCH 17/19] CI: attempt to fix translation resources --- scripts/build.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 84330572f..741f22c42 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" @@ -52,6 +51,8 @@ vite build $SRC_MAPS cp -r assets/layers/ dist/assets/layers/ cp -r assets/themes/ dist/assets/themes/ cp -r assets/svg/ dist/assets/svg/ +mkdir dist/assets/langs 2> /dev/null +mkdir dist/assets/langs/layers 2> /dev/null cp -r langs/layers/ dist/assets/langs/layers/ ls dist/assets/langs/layers/ export NODE_OPTIONS="" From 7362dc210f188ffe6c20eb109661a267876b8885 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 03:47:23 +0200 Subject: [PATCH 18/19] CI: attempt to fix build --- scripts/build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 741f22c42..33cc89061 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -51,8 +51,8 @@ vite build $SRC_MAPS cp -r assets/layers/ dist/assets/layers/ cp -r assets/themes/ dist/assets/themes/ cp -r assets/svg/ dist/assets/svg/ -mkdir dist/assets/langs 2> /dev/null -mkdir dist/assets/langs/layers 2> /dev/null +mkdir dist/assets/langs +mkdir dist/assets/langs/layers cp -r langs/layers/ dist/assets/langs/layers/ ls dist/assets/langs/layers/ export NODE_OPTIONS="" From 8f5ba2153a87394e0543bb2dad1d52e5f5ddab4b Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 04:09:18 +0200 Subject: [PATCH 19/19] CI: attempt to fix build --- scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index 33cc89061..b530e79ea 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -53,6 +53,6 @@ cp -r assets/themes/ dist/assets/themes/ cp -r assets/svg/ dist/assets/svg/ mkdir dist/assets/langs mkdir dist/assets/langs/layers -cp -r langs/layers/ dist/assets/langs/layers/ +cp -r langs/layers/ dist/assets/langs/ ls dist/assets/langs/layers/ export NODE_OPTIONS=""