Add summary layer
This commit is contained in:
parent
5b318236bf
commit
74fb4bd5d1
19 changed files with 533 additions and 88 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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": [
|
||||
|
|
25
assets/layers/summary/summary.json
Normal file
25
assets/layers/summary/summary.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -54,5 +54,7 @@
|
|||
"pharmacy",
|
||||
"ice_cream"
|
||||
],
|
||||
"widenFactor": 3
|
||||
"overideAll": {
|
||||
"minzoom": 16
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
])
|
||||
)
|
||||
|
|
|
@ -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(""),
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,6 @@ export interface SpecialVisualizationState {
|
|||
readonly preferencesAsTags: UIEventSource<Record<string, string>>
|
||||
readonly language: UIEventSource<string>
|
||||
}
|
||||
readonly lastClickObject: WritableFeatureSource
|
||||
|
||||
readonly availableLayers: Store<RasterLayerPolygon[]>
|
||||
|
||||
|
|
|
@ -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} />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue