Add summary layer

This commit is contained in:
Pieter Vander Vennet 2024-02-15 17:39:59 +01:00
parent 5b318236bf
commit 74fb4bd5d1
19 changed files with 533 additions and 88 deletions

View file

@ -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

View file

@ -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"

View file

@ -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": [

View file

@ -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"
}
]
}

View file

@ -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"
},
{

View file

@ -54,5 +54,7 @@
"pharmacy",
"ice_cream"
],
"widenFactor": 3
"overideAll": {
"minzoom": 16
}
}

View file

@ -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<string> = ["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<number> {
async getCount(
layer: string,
bbox: [[number, number], [number, number]] = undefined
): Promise<number> {
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<string[]> {
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<PoiDatabaseMeta> {
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 = <any>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<string>
}[]
) {
handle.push({
mustMatch: "",
mimetype: "text/html",
handle: async () => {
return `<html><body>Supported endpoints are <ul>${handle
.filter((h) => h.mustMatch !== "")
.map((h) => {
let l = h.mustMatch
if (typeof h.mustMatch === "string") {
l = `<a href='${l}'>${l}</a>`
}
return "<li>" + l + "</li>"
})
.join("")}</ul></body></html>`
},
})
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("<html><body><p>Not found...</p></body></html>")
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<string, number> = {}
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],
])
)

View file

@ -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<Feature[]> = new UIEventSource<Feature[]>([])
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<Point, OsmTags> {
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(""),

View file

@ -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<number>,
mapProperties: {
bounds: Store<BBox>
zoom: Store<number>
},
options?: {
isActive?: Store<boolean>
}
) {
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<Feature<Point>[]> = 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
)
}
}

View file

@ -638,8 +638,9 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
promise: Promise<T>
): 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
}

View file

@ -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[] = <any>Constants._defaultPinIcons
/**

View file

@ -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
? "<img src='https://mapcomplete.org/assets/svg/statistics.svg' height='18px'>"
: Svg.statistics_svg().SetClass("w-4 h-4 mr-2"),
"<img src='https://mapcomplete.org/assets/svg/statistics.svg' height='18px'>",
"https://taginfo.openstreetmap.org/keys/" + values.key + "#values",
true
),

View file

@ -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<string, string[]>
total: number

View file

@ -79,7 +79,6 @@ export default class PointRenderingConfig extends WithContextLoader {
}
})
this.marker = (json.marker ?? []).map((m) => new IconConfig(<any>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)

View file

@ -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<boolean> = new UIEventSource<boolean>(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(<any>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(<any>l.id) < 0)
.map((l) => l.minzoom)
)
console.log("Maxzoom is", maxzoom)
new ShowDataLayer(this.map, {
features: specialLayers.summary,
layer: new LayerConfig(<LayerConfigJson>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()
}

View file

@ -35,7 +35,6 @@
async function clicked() {
isExporting = true
const gpsLayer = state.layerState.filteredLayers.get(<PriviligedLayerType>"gps_location")
state.lastClickObject.features.setData([])
state.userRelatedState.preferencesAsTags.data["__showTimeSensitiveIcons"] = "no"
state.userRelatedState.preferencesAsTags.ping()
const gpsIsDisplayed = gpsLayer.isDisplayed.data

View file

@ -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
}

View file

@ -84,7 +84,6 @@ export interface SpecialVisualizationState {
readonly preferencesAsTags: UIEventSource<Record<string, string>>
readonly language: UIEventSource<string>
}
readonly lastClickObject: WritableFeatureSource
readonly availableLayers: Store<RasterLayerPolygon[]>

View file

@ -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 @@
<div class="flex w-full items-end justify-between px-4">
<div class="flex flex-col">
<If condition={featureSwitches.featureSwitchEnableLogin}>
{#if state.lastClickObject.hasPresets || state.lastClickObject.hasNoteLayer}
{#if state.layout.hasPresets() || state.layout.hasNoteLayer()}
<button
class="pointer-events-auto w-fit"
on:click={() => {
@ -267,7 +272,7 @@
}}
on:keydown={forwardEventToMap}
>
{#if state.lastClickObject.hasPresets}
{#if state.layout.hasPresets()}
<Tr t={Translations.t.general.add.title} />
{:else}
<Tr t={Translations.t.notes.addAComment} />