From 74fb4bd5d1c9a8c33e68d47c48663e736ba79661 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 15 Feb 2024 17:39:59 +0100 Subject: [PATCH] Add summary layer --- Docs/SettingUpPSQL.md | 11 + assets/layers/ice_cream/ice_cream.json | 1 + assets/layers/last_click/last_click.json | 2 +- assets/layers/summary/summary.json | 25 ++ assets/layers/toilet/toilet.json | 145 +++++++++-- assets/themes/shops/shops.json | 4 +- scripts/osm2pgsql/tilecountServer.ts | 232 ++++++++++++++++-- .../Sources/LastClickFeatureSource.ts | 20 +- .../TiledFeatureSource/SummaryTileSource.ts | 85 +++++++ src/Logic/UIEventSource.ts | 5 +- src/Models/Constants.ts | 4 +- src/Models/ThemeConfig/LayerConfig.ts | 5 +- src/Models/ThemeConfig/LayoutConfig.ts | 8 + .../ThemeConfig/PointRenderingConfig.ts | 3 +- src/Models/ThemeViewState.ts | 59 +++-- src/UI/DownloadFlow/DownloadButton.svelte | 1 - src/UI/Popup/AddNewPoint/AddNewPoint.svelte | 1 - src/UI/SpecialVisualization.ts | 1 - src/UI/ThemeViewGUI.svelte | 9 +- 19 files changed, 533 insertions(+), 88 deletions(-) create mode 100644 assets/layers/summary/summary.json create mode 100644 src/Logic/FeatureSource/TiledFeatureSource/SummaryTileSource.ts diff --git a/Docs/SettingUpPSQL.md b/Docs/SettingUpPSQL.md index e3a4ae13b..1b7ed9aa9 100644 --- a/Docs/SettingUpPSQL.md +++ b/Docs/SettingUpPSQL.md @@ -36,6 +36,17 @@ Storing properties to table '"public"."osm2pgsql_properties" takes about 25 minu Belgium (~555mb) takes 15m World (80GB) should take 15m*160 = 2400m = 40hr +73G Jan 23 00:22 planet-240115.osm.pbf: 2024-02-10 16:45:11 osm2pgsql took 871615s (242h 6m 55s; 10 days) overall on lain.local with RAID5 on 4 HDD disks, database is over 1Terrabyte (!) + +Server specs + +Lenovo thinkserver RD350, Intel Xeon E5-2600, 2Rx4 PC3 + 11 watt powered off, 73 watt idle, ~100 watt when importing + +HP ProLiant DL360 G7 (1U): 2Rx4 DDR3-memory (PC3) + Intel Xeon X56** + + ## Deploying a tile server diff --git a/assets/layers/ice_cream/ice_cream.json b/assets/layers/ice_cream/ice_cream.json index b6d6576a9..8b5ea0568 100644 --- a/assets/layers/ice_cream/ice_cream.json +++ b/assets/layers/ice_cream/ice_cream.json @@ -4,6 +4,7 @@ "en": "Ice cream parlors", "de": "Eisdielen" }, + "minzoom": 14, "description": { "en": "A place where ice cream is sold over the counter", "de": "Ein Ort, an dem Eiscreme an der Theke verkauft wird" diff --git a/assets/layers/last_click/last_click.json b/assets/layers/last_click/last_click.json index c99a4ce5b..335731bda 100644 --- a/assets/layers/last_click/last_click.json +++ b/assets/layers/last_click/last_click.json @@ -1,7 +1,7 @@ { "id": "last_click", "name": null, - "description": "This layer defines how to render the 'last click'-location. By default, it will show a marker with the possibility to add a new point (if there are some presets) and/or to add a new note (if the 'note' layer attribute is set). If none are possible, this layer won't show up", + "description": "This 'layer' is not really a layer, but contains part of the code how the popup to 'add a new marker' is displayed", "source": "special", "isShown": { "or": [ diff --git a/assets/layers/summary/summary.json b/assets/layers/summary/summary.json new file mode 100644 index 000000000..ecffca76a --- /dev/null +++ b/assets/layers/summary/summary.json @@ -0,0 +1,25 @@ +{ + "id": "summary", + "description": "Special layer which shows `count`", + "source": "special", + "name": "CLusters", + "title": { + "render": {"en": "Summary"} + }, + "tagRenderings": [ + "all_tags" + ], + "pointRendering": [ + { + "location": [ + "point", + "centroid" + ], + "iconSize": "25,25", + "label": { + "render": "{total}" + }, + "labelCssClasses": "bg-white w-6 h-6 text-lg rounded-full" + } + ] +} diff --git a/assets/layers/toilet/toilet.json b/assets/layers/toilet/toilet.json index 2d58241b6..192a60ab7 100644 --- a/assets/layers/toilet/toilet.json +++ b/assets/layers/toilet/toilet.json @@ -26,7 +26,7 @@ "source": { "osmTags": "amenity=toilets" }, - "minzoom": 9, + "minzoom": 10, "title": { "render": { "en": "Toilet", @@ -548,6 +548,115 @@ } ] }, + { + "condition": "toilets:position!=urinal", + "id": "gender_segregated", + "question": { + "en": "Are these toilets gender-segregated?", + "nl": "Zijn deze toiletten gescheiden op basis van geslacht?" + }, + "questionHint": { + "en": "Are there separate stalls or separate areas for men and women and are they signposted as such?", + "nl": "Is er een aparte ruimte voor mannen en vrouwen en zijn deze ruimtes ook expliciet aangegeven?" + }, + "mappings": [ + { + "if": "gender_segregated=yes", + "then": { + "en": "There is a separate, signposted area for men and women", + "nl": "Er zijn aparte ruimtes of toiletten voor mannen en vrouwen" + } + }, + { + "if": "gender_segregated=no", + "then": { + "en": "There is no separate, signposted area for men and women", + "nl": "Mannen en vrouwen gebruiken dezelfde ruimtes en toiletten" + } + } + ] + }, + { + "id": "menstrual_products", + "question": { + "en": "Are free, menstrual products distributed here?", + "nl": "Zijn er gratis menstruatieproducten beschikbaar?" + }, + "questionHint": { + "en": "This is only about menstrual products that are free of charge. If e.g. a vending machine is available which charges for menstrual products, ignore it for this question.", + "nl": "Dit gaat enkel over menstruatieproducten die gratis geschikbaar zijn. Indien er bv. een verkoopautomaat met menstruatieproducten is, negeer deze dan" + }, + "mappings": [ + { + "if": "toilets:menstrual_products=yes", + "then": { + "en": "Free menstrual products are available to all visitors of these toilets", + "nl": "Er zijn gratis menstruatieprocten beschikbaar voor alle bezoekers van deze toiletten" + } + }, + { + "if": "toilets:menstrual_products=limited", + "then": { + "en": "Free menstrual products are available to some visitors of these toilets", + "nl": "De gratis menstruatieproducten zijn enkel beschikbaar in een deel van de toiletten" + }, + "hideInAnswer": "gender_segregated=yes" + }, + { + "if": "toilets:menstrual_products=no", + "alsoShowIf": "toilets:menstrual_products=", + "then": { + "en": "No free menstrual products are available here", + "nl": "Er zijn geen gratis menstruatieproducten beschikbaar" + } + } + ] + }, + { + "id": "menstrual_products_location", + "question": { + "en": "Where are the free menstrual products located?", + "nl": "Waar bevinden de gratis menstruatieproducten zich?" + }, + "condition": { + "or": [ + "toilets:menstrual_products=limited", + "toilets:menstrual_products:location~*" + ] + }, + "render": { + "en": "The menstrual products are located in {toilets:menstrual_products:location}", + "nl": "De menstruatieproducten bevinden zich in {toilets:menstrual_products:location}" + }, + "freeform": { + "key": "toilets:menstrual_products:location", + "inline": true + }, + "mappings": [ + { + "then": { + "en": "The free, menstrual products are located in the toilet for women", + "nl": "De gratis menstruatieproducten bevinden zich in het vrouwentoilet" + }, + "if": "toilets:menstrual_products:location=female_toilet", + "alsoShowIf": "toilets:menstrual_products:location=" + }, + { + "then": { + "en": "The free, menstrual products are located in the toilet for men", + "nl": "De gratis menstruatieproducten bevinden zich in het mannentoilet" + }, + "if": "toilets:menstrual_products:location=male_toilet" + }, + { + "if": "toilets:menstrual_products:location=wheelchair_toilet", + "then": { + "en": "The free, menstrual products are located in the toilet for wheelchair users", + "nl": "De gratis menstruatieproducten bevinden zich in het rolstoeltoegankelijke toilet" + } + } + ] + }, { "id": "toilets-changing-table", "labels": [ @@ -576,7 +685,8 @@ "ca": "Hi ha un canviador per a nadons", "cs": "Přebalovací pult je k dispozici" }, - "if": "changing_table=yes" + "if": "changing_table=yes", + "icon": "./assets/layers/toilet/baby.svg" }, { "if": "changing_table=no", @@ -610,10 +720,10 @@ "cs": "Kde je umístěn přebalovací pult?" }, "render": { - "en": "The changing table is located at {changing_table:location}", - "de": "Die Wickeltabelle befindet sich in {changing_table:location}", + "en": "A changing table is located at {changing_table:location}", + "de": "Ein Wickeltisch befindet sich in {changing_table:location}", "fr": "Emplacement de la table à langer : {changing_table:location}", - "nl": "De luiertafel bevindt zich in {changing_table:location}", + "nl": "Er bevindt zich een luiertafel in {changing_table:location}", "it": "Il fasciatoio si trova presso {changing_table:location}", "es": "El cambiador está en {changing_table:location}", "da": "Puslebordet er placeret på {changing_table:location}", @@ -632,10 +742,10 @@ "mappings": [ { "then": { - "en": "The changing table is in the toilet for women. ", - "de": "Der Wickeltisch befindet sich in der Damentoilette. ", + "en": "A changing table is in the toilet for women", + "de": "Ein Wickeltisch ist in der Damentoilette vorhanden", "fr": "La table à langer est dans les toilettes pour femmes. ", - "nl": "De luiertafel bevindt zich in de vrouwentoiletten ", + "nl": "Er bevindt zich een luiertafel in de vrouwentoiletten ", "it": "Il fasciatoio è nei servizi igienici femminili. ", "da": "Puslebordet er på toilettet til kvinder. ", "ca": "El canviador està al lavabo per a dones. ", @@ -645,10 +755,10 @@ }, { "then": { - "en": "The changing table is in the toilet for men. ", - "de": "Der Wickeltisch befindet sich in der Herrentoilette. ", + "en": "A changing table is in the toilet for men", + "de": "Ein Wickeltisch ist in der Herrentoilette vorhanden", "fr": "La table à langer est dans les toilettes pour hommes. ", - "nl": "De luiertafel bevindt zich in de herentoiletten ", + "nl": "Er bevindt zich een luiertafel in de herentoiletten ", "it": "Il fasciatoio è nei servizi igienici maschili. ", "ca": "El canviador està al lavabo per a homes. ", "cs": "Přebalovací pult je na pánské toaletě. " @@ -658,10 +768,10 @@ { "if": "changing_table:location=wheelchair_toilet", "then": { - "en": "The changing table is in the toilet for wheelchair users. ", - "de": "Der Wickeltisch befindet sich in der Toilette für Rollstuhlfahrer. ", + "en": "A changing table is in the toilet for wheelchair users", + "de": "Ein Wickeltisch ist in der barrierefreien Toilette vorhanden", "fr": "La table à langer est dans les toilettes pour personnes à mobilité réduite. ", - "nl": "De luiertafel bevindt zich in de rolstoeltoegankelijke toilet ", + "nl": "Er bevindt zich een luiertafel in de rolstoeltoegankelijke toilet ", "it": "Il fasciatoio è nei servizi igienici per persone in sedia a rotelle. ", "da": "Puslebordet er på toilettet for kørestolsbrugere. ", "ca": "El canviador està al lavabo per a usuaris de cadira de rodes. ", @@ -671,10 +781,10 @@ { "if": "changing_table:location=dedicated_room", "then": { - "en": "The changing table is in a dedicated room. ", - "de": "Der Wickeltisch befindet sich in einem eigenen Raum. ", + "en": "A changing table is in a dedicated room", + "de": "Ein Wickeltisch befindet sich in einem eigenen Raum", "fr": "La table à langer est dans un espace dédié. ", - "nl": "De luiertafel bevindt zich in een daartoe voorziene kamer ", + "nl": "Er bevindt zich een luiertafel in een daartoe voorziene kamer ", "it": "Il fasciatoio è in una stanza dedicata. ", "es": "El cambiador está en una habitación dedicada ", "da": "Vuggestuen står i et særligt rum. ", @@ -683,6 +793,7 @@ } } ], + "multiAnswer": true, "id": "toilet-changing_table:location" }, { diff --git a/assets/themes/shops/shops.json b/assets/themes/shops/shops.json index 0024d9837..abe44dc0e 100644 --- a/assets/themes/shops/shops.json +++ b/assets/themes/shops/shops.json @@ -54,5 +54,7 @@ "pharmacy", "ice_cream" ], - "widenFactor": 3 + "overideAll": { + "minzoom": 16 + } } diff --git a/scripts/osm2pgsql/tilecountServer.ts b/scripts/osm2pgsql/tilecountServer.ts index 5c33c72b7..8980e9939 100644 --- a/scripts/osm2pgsql/tilecountServer.ts +++ b/scripts/osm2pgsql/tilecountServer.ts @@ -1,44 +1,240 @@ -import { BBox } from "../../src/Logic/BBox" import { Client } from "pg" +import http from "node:http" +import { Tiles } from "../../src/Models/TileRange" + +/** + * Just the OSM2PGSL default database + */ +interface PoiDatabaseMeta { + attributes + current_timestamp + db_format + flat_node_file + import_timestamp + output + prefix + replication_base_url + replication_sequence_number + replication_timestamp + style + updatable + version +} /** * Connects with a Postgis database, gives back how much items there are within the given BBOX */ -export default class TilecountServer { +class OsmPoiDatabase { + private static readonly prefixes: ReadonlyArray = ["pois", "lines", "polygons"] private readonly _client: Client private isConnected = false + private supportedLayers: string[] = undefined + private metaCache: PoiDatabaseMeta = undefined + private metaCacheDate: Date = undefined constructor(connectionString: string) { this._client = new Client(connectionString) } - async getCount(layer: string, bbox: BBox = undefined): Promise { + async getCount( + layer: string, + bbox: [[number, number], [number, number]] = undefined + ): Promise { if (!this.isConnected) { await this._client.connect() this.isConnected = true } - let query = "SELECT COUNT(*) FROM " + layer + let total = 0 - if(bbox){ - query += ` WHERE ST_MakeEnvelope (${bbox.minLon}, ${bbox.minLat}, ${bbox.maxLon}, ${bbox.maxLat}, 4326) ~ geom` + for (const prefix of OsmPoiDatabase.prefixes) { + let query = "SELECT COUNT(*) FROM " + prefix + "_" + layer + + if (bbox) { + query += ` WHERE ST_MakeEnvelope (${bbox[0][0]}, ${bbox[0][1]}, ${bbox[1][0]}, ${bbox[1][1]}, 4326) ~ geom` + } + console.log("Query:", query) + const result = await this._client.query(query) + total += Number(result.rows[0].count) } -console.log(query) - const result = await this._client.query(query) - return result.rows[0].count + return total } disconnect() { this._client.end() } + + async getLayers(): Promise { + if (this.supportedLayers !== undefined) { + return this.supportedLayers + } + const result = await this._client.query( + "SELECT table_name \n" + + "FROM information_schema.tables \n" + + "WHERE table_schema = 'public' AND table_name LIKE 'lines_%';" + ) + const layers = result.rows.map((r) => r.table_name.substring("lines_".length)) + this.supportedLayers = layers + return layers + } + + async getMeta(): Promise { + const now = new Date() + if (this.metaCache !== undefined) { + const diffSec = (this.metaCacheDate.getTime() - now.getTime()) / 1000 + if (diffSec < 120) { + return this.metaCache + } + } + const result = await this._client.query("SELECT * FROM public.osm2pgsql_properties") + const meta = {} + for (const { property, value } of result.rows) { + meta[property] = value + } + this.metaCacheDate = now + this.metaCache = meta + return this.metaCache + } } -const tcs = new TilecountServer("postgresql://user:none@localhost:5444/osm-poi") -console.log(">>>", await tcs.getCount("drinking_water", new BBox([ - [1.5052013991654007, - 42.57480750272123, - ], [ - 1.6663677350703097, - 42.499856652770745, - ]]))) -tcs.disconnect() +class Server { + constructor( + port: number, + handle: { + mustMatch: string | RegExp + mimetype: string + handle: (path: string) => Promise + }[] + ) { + handle.push({ + mustMatch: "", + mimetype: "text/html", + handle: async () => { + return `Supported endpoints are
    ${handle + .filter((h) => h.mustMatch !== "") + .map((h) => { + let l = h.mustMatch + if (typeof h.mustMatch === "string") { + l = `${l}` + } + return "
  • " + l + "
  • " + }) + .join("")}
` + }, + }) + http.createServer(async (req: http.IncomingMessage, res) => { + try { + console.log( + req.method + " " + req.url, + "from:", + req.headers.origin, + new Date().toISOString() + ) + + const url = new URL(`http://127.0.0.1/` + req.url) + let path = url.pathname + while (path.startsWith("/")) { + path = path.substring(1) + } + const handler = handle.find((h) => { + if (typeof h.mustMatch === "string") { + return h.mustMatch === path + } + if (path.match(h.mustMatch)) { + return true + } + }) + + if (handler === undefined || handler === null) { + res.writeHead(404, { "Content-Type": "text/html" }) + res.write("

Not found...

") + res.end() + return + } + + res.setHeader( + "Access-Control-Allow-Headers", + "Origin, X-Requested-With, Content-Type, Accept" + ) + res.setHeader("Access-Control-Allow-Origin", req.headers.origin ?? "*") + if (req.method === "OPTIONS") { + res.setHeader( + "Access-Control-Allow-Methods", + "POST, GET, OPTIONS, DELETE, UPDATE" + ) + res.writeHead(204, { "Content-Type": handler.mimetype }) + res.end() + return + } + if (req.method === "POST" || req.method === "UPDATE") { + return + } + + if (req.method === "DELETE") { + return + } + + try { + const result = await handler.handle(path) + res.writeHead(200, { "Content-Type": handler.mimetype }) + res.write(result) + res.end() + } catch (e) { + console.error("Could not handle request:", e) + res.writeHead(500) + res.write(e) + res.end() + } + } catch (e) { + console.error("FATAL:", e) + res.end() + } + }).listen(port) + console.log( + "Server is running on port " + port, + ". Supported endpoints are: " + handle.map((h) => h.mustMatch).join(", ") + ) + } +} + +const connectionString = "postgresql://user:password@localhost:5444/osm-poi" +const tcs = new OsmPoiDatabase(connectionString) +const server = new Server(2345, [ + { + mustMatch: "status.json", + mimetype: "application/json", + handle: async (path: string) => { + const layers = await tcs.getLayers() + const meta = await tcs.getMeta() + return JSON.stringify({ meta, layers }) + }, + }, + { + mustMatch: /[a-zA-Z0-9+]+\/[0-9]+\/[0-9]+\/[0-9]+\.json/, + mimetype: "application/json", // "application/vnd.geo+json", + async handle(path) { + console.log("Path is:", path, path.split(".")[0]) + const [layers, z, x, y] = path.split(".")[0].split("/") + + let sum = 0 + let properties: Record = {} + for (const layer of layers.split("+")) { + const count = await tcs.getCount( + layer, + Tiles.tile_bounds_lon_lat(Number(z), Number(x), Number(y)) + ) + properties[layer] = count + sum += count + } + + return JSON.stringify({ ...properties, total: sum }) + }, + }, +]) +console.log( + ">>>", + await tcs.getCount("drinking_water", [ + [3.194358020772171, 51.228073636083394], + [3.2839964396059145, 51.172701162680994], + ]) +) diff --git a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts index c95857d70..3fe9b79e9 100644 --- a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts @@ -11,16 +11,15 @@ import { OsmTags } from "../../../Models/OsmFeature" * Highly specialized feature source. * 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([]) - public readonly hasNoteLayer: boolean +export class LastClickFeatureSource { public readonly renderings: string[] - public readonly hasPresets: boolean private i: number = 0 + private readonly hasPresets: boolean + private readonly hasNoteLayer: boolean - constructor(location: Store<{ lon: number; lat: number }>, layout: LayoutConfig) { - this.hasNoteLayer = layout.layers.some((l) => l.id === "note") - this.hasPresets = layout.layers.some((l) => l.presets?.length > 0) + constructor(layout: LayoutConfig) { + this.hasNoteLayer = layout.hasNoteLayer() + this.hasPresets = layout.hasPresets() const allPresets: BaseUIElement[] = [] for (const layer of layout.layers) for (let i = 0; i < (layer.presets ?? []).length; i++) { @@ -43,16 +42,11 @@ export class LastClickFeatureSource implements WritableFeatureSource { Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML ) ) - - location.addCallbackAndRunD(({ 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, + id: "new_point_dialog", has_note_layer: this.hasNoteLayer ? "yes" : "no", has_presets: this.hasPresets ? "yes" : "no", renderings: this.renderings.join(""), diff --git a/src/Logic/FeatureSource/TiledFeatureSource/SummaryTileSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/SummaryTileSource.ts new file mode 100644 index 000000000..2a7105fca --- /dev/null +++ b/src/Logic/FeatureSource/TiledFeatureSource/SummaryTileSource.ts @@ -0,0 +1,85 @@ +import DynamicTileSource from "./DynamicTileSource" +import { Store, UIEventSource } from "../../UIEventSource" +import { BBox } from "../../BBox" +import StaticFeatureSource from "../Sources/StaticFeatureSource" +import { Feature, Point } from "geojson" +import { Utils } from "../../../Utils" +import { Tiles } from "../../../Models/TileRange" + +/** + * Provides features summarizing the total amount of features at a given location + */ +export class SummaryTileSource extends DynamicTileSource { + private static readonly empty = [] + constructor( + cacheserver: string, + layers: string[], + zoomRounded: Store, + mapProperties: { + bounds: Store + zoom: Store + }, + options?: { + isActive?: Store + } + ) { + const layersSummed = layers.join("+") + super( + zoomRounded, + 0, // minzoom + (tileIndex) => { + const [z, x, y] = Tiles.tile_from_index(tileIndex) + const coordinates = Tiles.centerPointOf(z, x, y) + + const count = UIEventSource.FromPromiseWithErr( + Utils.downloadJson(`${cacheserver}/${layersSummed}/${z}/${x}/${y}.json`) + ) + const features: Store[]> = count.mapD((count) => { + if (count["error"] !== undefined) { + console.error( + "Could not download count for tile", + z, + x, + y, + "due to", + count["error"] + ) + return SummaryTileSource.empty + } + const counts = count["success"] + if (counts === undefined || counts["total"] === 0) { + return SummaryTileSource.empty + } + return [ + { + type: "Feature", + properties: { + id: "summary_" + tileIndex, + summary: "yes", + ...counts, + layers: layersSummed, + }, + geometry: { + type: "Point", + coordinates, + }, + }, + ] + }) + return new StaticFeatureSource( + features.map( + (f) => { + if (z !== zoomRounded.data) { + return SummaryTileSource.empty + } + return f + }, + [zoomRounded] + ) + ) + }, + mapProperties, + options + ) + } +} diff --git a/src/Logic/UIEventSource.ts b/src/Logic/UIEventSource.ts index ed48130ff..48bc9c6d9 100644 --- a/src/Logic/UIEventSource.ts +++ b/src/Logic/UIEventSource.ts @@ -638,8 +638,9 @@ export class UIEventSource extends Store implements Writable { promise: Promise ): UIEventSource<{ success: T } | { error: any } | undefined> { const src = new UIEventSource<{ success: T } | { error: any }>(undefined) - promise?.then((d) => src.setData({ success: d })) - promise?.catch((err) => src.setData({ error: err })) + promise + ?.then((d) => src.setData({ success: d })) + ?.catch((err) => src.setData({ error: err })) return src } diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts index 0d76b4fc5..31181c5e6 100644 --- a/src/Models/Constants.ts +++ b/src/Models/Constants.ts @@ -24,6 +24,7 @@ export default class Constants { "range", "last_click", "favourite", + "summary", ] as const /** * Special layers which are not included in a theme by default @@ -36,7 +37,7 @@ export default class Constants { "import_candidate", "usersettings", "icons", - "filters" + "filters", ] as const /** * Layer IDs of layers which have special properties through built-in hooks @@ -151,7 +152,6 @@ export default class Constants { "mastodon", "party", "addSmall", - ] as const public static readonly defaultPinIcons: string[] = Constants._defaultPinIcons /** diff --git a/src/Models/ThemeConfig/LayerConfig.ts b/src/Models/ThemeConfig/LayerConfig.ts index 7b2feacd0..0f8b5facc 100644 --- a/src/Models/ThemeConfig/LayerConfig.ts +++ b/src/Models/ThemeConfig/LayerConfig.ts @@ -45,7 +45,6 @@ export default class LayerConfig extends WithContextLoader { public readonly isShown: TagsFilter public minzoom: number public minzoomVisible: number - public readonly maxzoom: number public readonly title?: TagRenderingConfig public readonly titleIcons: TagRenderingConfig[] public readonly mapRendering: PointRenderingConfig[] @@ -464,9 +463,7 @@ export default class LayerConfig extends WithContextLoader { return [ new Combine([ new Link( - Utils.runningFromConsole - ? "" - : Svg.statistics_svg().SetClass("w-4 h-4 mr-2"), + "", "https://taginfo.openstreetmap.org/keys/" + values.key + "#values", true ), diff --git a/src/Models/ThemeConfig/LayoutConfig.ts b/src/Models/ThemeConfig/LayoutConfig.ts index c9859b2c5..194089aeb 100644 --- a/src/Models/ThemeConfig/LayoutConfig.ts +++ b/src/Models/ThemeConfig/LayoutConfig.ts @@ -245,6 +245,14 @@ export default class LayoutConfig implements LayoutInformation { return this.layers.some((l) => l.isLeftRightSensitive()) } + public hasNoteLayer() { + return this.layers.some((l) => l.id === "note") + } + + public hasPresets() { + return this.layers.some((l) => l.presets?.length > 0) + } + public missingTranslations(extraInspection: any): { untranslated: Map total: number diff --git a/src/Models/ThemeConfig/PointRenderingConfig.ts b/src/Models/ThemeConfig/PointRenderingConfig.ts index a522c45a7..5f7974723 100644 --- a/src/Models/ThemeConfig/PointRenderingConfig.ts +++ b/src/Models/ThemeConfig/PointRenderingConfig.ts @@ -79,7 +79,6 @@ export default class PointRenderingConfig extends WithContextLoader { } }) - this.marker = (json.marker ?? []).map((m) => new IconConfig(m)) if (json.css !== undefined) { this.cssDef = this.tr("css", undefined) @@ -307,7 +306,7 @@ export default class PointRenderingConfig extends WithContextLoader { const label = self.label ?.GetRenderValue(tags) ?.Subs(tags) - ?.SetClass("block center absolute text-center marker-label") + ?.SetClass("flex items-center justify-center absolute marker-label") ?.SetClass(cssClassesLabel) if (cssLabel) { label.SetStyle(cssLabel) diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 011cf335a..83602f151 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -62,7 +62,9 @@ import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFe import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider" import { GeolocationControlState } from "../UI/BigComponents/GeolocationControl" import Zoomcontrol from "../UI/Zoomcontrol" - +import { SummaryTileSource } from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource" +import summaryLayer from "../assets/generated/layers/summary.json" +import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson" /** * * The themeviewState contains all the state needed for the themeViewGUI. @@ -140,7 +142,6 @@ export default class ThemeViewState implements SpecialVisualizationState { * Triggered by navigating the map with arrows or by pressing 'space' or 'enter' */ public readonly visualFeedback: UIEventSource = new UIEventSource(false) - private readonly newPointDialog: FilteredLayer constructor(layout: LayoutConfig) { Utils.initDomPurify() @@ -309,7 +310,6 @@ export default class ThemeViewState implements SpecialVisualizationState { fs.layer.layerDef.maxAgeOfCache ) }) - this.newPointDialog = this.layerState.filteredLayers.get("last_click") this.floors = this.featuresInView.features.stabilized(500).map((features) => { if (!features) { @@ -343,10 +343,7 @@ export default class ThemeViewState implements SpecialVisualizationState { return sorted }) - this.lastClickObject = new LastClickFeatureSource( - this.mapProperties.lastClickLocation, - this.layout - ) + this.lastClickObject = new LastClickFeatureSource(this.layout) this.osmObjectDownloader = new OsmObjectDownloader( this.osmConnection.Backend(), @@ -446,7 +443,6 @@ export default class ThemeViewState implements SpecialVisualizationState { const feature = this.lastClickObject.createFeature(lon, lat) this.featureProperties.trackFeature(feature) this.selectedElement.setData(feature) - this.selectedLayer.setData(this.newPointDialog.layerDef) } /** @@ -472,16 +468,6 @@ export default class ThemeViewState implements SpecialVisualizationState { 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([]) - }) - this.selectedElement.addCallback((selected) => { if (selected === undefined) { Zoomcontrol.resetzoom() @@ -656,6 +642,19 @@ export default class ThemeViewState implements SpecialVisualizationState { }) } + private setupSummaryLayer() { + const layers = this.layout.layers.filter( + (l) => + Constants.priviliged_layers.indexOf(l.id) < 0 && + l.source.geojsonSource === undefined + ) + return new SummaryTileSource( + "http://127.0.0.1:2345", + layers.map((l) => l.id), + this.mapProperties.zoom.map((z) => Math.max(Math.ceil(z) + 1, 0)), + this.mapProperties + ) + } /** * Add the special layers to the map */ @@ -683,6 +682,7 @@ export default class ThemeViewState implements SpecialVisualizationState { ), current_view: this.currentView, favourite: this.favourites, + summary: this.setupSummaryLayer(), } this.closestFeatures.registerSource(specialLayers.favourite, "favourite") @@ -720,15 +720,16 @@ export default class ThemeViewState implements SpecialVisualizationState { rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true) } - // enumarate all 'normal' layers and match them with the appropriate 'special' layer - if applicable + // enumerate all 'normal' layers and match them with the appropriate 'special' layer - if applicable this.layerState.filteredLayers.forEach((flayer) => { const id = flayer.layerDef.id const features: FeatureSource = specialLayers[id] if (features === undefined) { return } - if (id === "favourite") { - console.log("Matching special layer", id, flayer) + if (id === "summary") { + console.log("Skipping summary!") + return } this.featureProperties.trackFeatureSource(features) @@ -741,6 +742,20 @@ export default class ThemeViewState implements SpecialVisualizationState { selectedLayer: this.selectedLayer, }) }) + + const maxzoom = Math.min( + ...this.layout.layers + .filter((l) => Constants.priviliged_layers.indexOf(l.id) < 0) + .map((l) => l.minzoom) + ) + console.log("Maxzoom is", maxzoom) + new ShowDataLayer(this.map, { + features: specialLayers.summary, + layer: new LayerConfig(summaryLayer, "summaryLayer"), + doShowLayer: this.mapProperties.zoom.map((z) => z < maxzoom), + selectedLayer: this.selectedLayer, + selectedElement: this.selectedElement, + }) } /** @@ -761,8 +776,6 @@ export default class ThemeViewState implements SpecialVisualizationState { 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) this.focusOnMap() } diff --git a/src/UI/DownloadFlow/DownloadButton.svelte b/src/UI/DownloadFlow/DownloadButton.svelte index 229b74b9c..a8d0b1c72 100644 --- a/src/UI/DownloadFlow/DownloadButton.svelte +++ b/src/UI/DownloadFlow/DownloadButton.svelte @@ -35,7 +35,6 @@ async function clicked() { isExporting = true const gpsLayer = state.layerState.filteredLayers.get("gps_location") - state.lastClickObject.features.setData([]) state.userRelatedState.preferencesAsTags.data["__showTimeSensitiveIcons"] = "no" state.userRelatedState.preferencesAsTags.ping() const gpsIsDisplayed = gpsLayer.isDisplayed.data diff --git a/src/UI/Popup/AddNewPoint/AddNewPoint.svelte b/src/UI/Popup/AddNewPoint/AddNewPoint.svelte index d28c573fa..b1da84275 100644 --- a/src/UI/Popup/AddNewPoint/AddNewPoint.svelte +++ b/src/UI/Popup/AddNewPoint/AddNewPoint.svelte @@ -89,7 +89,6 @@ state.selectedElement.setData(undefined) // When aborted, we force the contributors to place the pin _again_ // This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map - state.lastClickObject.features.setData([]) preciseInputIsTapped = false } diff --git a/src/UI/SpecialVisualization.ts b/src/UI/SpecialVisualization.ts index 2799a2e80..ab61da599 100644 --- a/src/UI/SpecialVisualization.ts +++ b/src/UI/SpecialVisualization.ts @@ -84,7 +84,6 @@ export interface SpecialVisualizationState { readonly preferencesAsTags: UIEventSource> readonly language: UIEventSource } - readonly lastClickObject: WritableFeatureSource readonly availableLayers: Store diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 26e4f6e98..f6ee40867 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -96,6 +96,11 @@ if (element.properties.id.startsWith("current_view")) { return currentViewLayer } + console.log(">>> selected:", element) + if(element.properties.id === "new_point_dialog"){ + console.log(">>> searching last_click layer", layout) + return layout.layers.find(l => l.id === "last_click") + } if(element.properties.id === "location_track"){ return layout.layers.find(l => l.id === "gps_track") } @@ -259,7 +264,7 @@
- {#if state.lastClickObject.hasPresets || state.lastClickObject.hasNoteLayer} + {#if state.layout.hasPresets() || state.layout.hasNoteLayer()}