| 
									
										
										
										
											2024-01-22 01:42:05 +01:00
										 |  |  | import { Client } from "pg" | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  | 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 | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-01-22 01:42:05 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Connects with a Postgis database, gives back how much items there are within the given BBOX | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  | class OsmPoiDatabase { | 
					
						
							|  |  |  |     private static readonly prefixes: ReadonlyArray<string> = ["pois", "lines", "polygons"] | 
					
						
							| 
									
										
										
										
											2024-01-22 01:42:05 +01:00
										 |  |  |     private readonly _client: Client | 
					
						
							|  |  |  |     private isConnected = false | 
					
						
							| 
									
										
										
										
											2024-02-18 15:59:28 +01:00
										 |  |  |     private supportedLayers: Set<string> = undefined | 
					
						
							| 
									
										
										
										
											2024-02-18 17:38:29 +01:00
										 |  |  |     private supportedLayersDate: Date = undefined | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |     private metaCache: PoiDatabaseMeta = undefined | 
					
						
							|  |  |  |     private metaCacheDate: Date = undefined | 
					
						
							| 
									
										
										
										
											2024-01-22 01:42:05 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     constructor(connectionString: string) { | 
					
						
							|  |  |  |         this._client = new Client(connectionString) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |     async getCount( | 
					
						
							|  |  |  |         layer: string, | 
					
						
							|  |  |  |         bbox: [[number, number], [number, number]] = undefined | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  |     ): Promise<{ count: number; lat: number; lon: number }> { | 
					
						
							| 
									
										
										
										
											2024-01-22 01:42:05 +01:00
										 |  |  |         if (!this.isConnected) { | 
					
						
							|  |  |  |             await this._client.connect() | 
					
						
							|  |  |  |             this.isConnected = true | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  |         let total: number = 0 | 
					
						
							|  |  |  |         let latSum = 0 | 
					
						
							|  |  |  |         let lonSum = 0 | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |         for (const prefix of OsmPoiDatabase.prefixes) { | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  |             let query = | 
					
						
							|  |  |  |                 "SELECT COUNT(*), ST_AsText(ST_Centroid(ST_Collect(geom))) FROM " + | 
					
						
							|  |  |  |                 prefix + | 
					
						
							|  |  |  |                 "_" + | 
					
						
							|  |  |  |                 layer | 
					
						
							| 
									
										
										
										
											2024-01-22 01:42:05 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |             if (bbox) { | 
					
						
							|  |  |  |                 query += ` WHERE ST_MakeEnvelope (${bbox[0][0]}, ${bbox[0][1]}, ${bbox[1][0]}, ${bbox[1][1]}, 4326) ~ geom` | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             const result = await this._client.query(query) | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  |             const count = Number(result.rows[0].count) | 
					
						
							|  |  |  |             let point = result.rows[0].st_astext | 
					
						
							|  |  |  |             if (count === 0) { | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             total += count | 
					
						
							|  |  |  |             if (!point) { | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             point = point.substring(6, point.length - 1) | 
					
						
							|  |  |  |             const [lon, lat] = point.split(" ") | 
					
						
							|  |  |  |             latSum += lat * count | 
					
						
							|  |  |  |             lonSum += lon * count | 
					
						
							| 
									
										
										
										
											2024-01-22 01:42:05 +01:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         return { count: total, lat: latSum / total, lon: lonSum / total } | 
					
						
							| 
									
										
										
										
											2024-01-22 01:42:05 +01:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     disconnect() { | 
					
						
							|  |  |  |         this._client.end() | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-18 15:59:28 +01:00
										 |  |  |     async getLayers(): Promise<Set<string>> { | 
					
						
							| 
									
										
										
										
											2024-02-18 17:38:29 +01:00
										 |  |  |         if ( | 
					
						
							|  |  |  |             this.supportedLayers !== undefined && | 
					
						
							|  |  |  |             new Date().getTime() - this.supportedLayersDate.getTime() < 1000 * 60 * 60 * 24 | 
					
						
							|  |  |  |         ) { | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |             return this.supportedLayers | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-02-18 17:38:29 +01:00
										 |  |  |         const q = | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |             "SELECT table_name \n" + | 
					
						
							| 
									
										
										
										
											2024-02-18 17:38:29 +01:00
										 |  |  |             "FROM information_schema.tables \n" + | 
					
						
							|  |  |  |             "WHERE table_schema = 'public' AND table_name LIKE 'lines_%';" | 
					
						
							|  |  |  |         const result = await this._client.query(q) | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |         const layers = result.rows.map((r) => r.table_name.substring("lines_".length)) | 
					
						
							| 
									
										
										
										
											2024-02-18 15:59:28 +01:00
										 |  |  |         this.supportedLayers = new Set(layers) | 
					
						
							| 
									
										
										
										
											2024-02-18 17:38:29 +01:00
										 |  |  |         this.supportedLayersDate = new Date() | 
					
						
							| 
									
										
										
										
											2024-02-18 15:59:28 +01:00
										 |  |  |         return this.supportedLayers | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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 | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-01-22 01:42:05 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 14:22:16 +01:00
										 |  |  | class CachedSqlCount { | 
					
						
							|  |  |  |     private readonly _cache: Record< | 
					
						
							|  |  |  |         string, | 
					
						
							|  |  |  |         Record< | 
					
						
							|  |  |  |             number, | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 date: Date | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  |                 entry: { count: number; lat: number; lon: number } | 
					
						
							| 
									
										
										
										
											2024-02-19 14:22:16 +01:00
										 |  |  |             } | 
					
						
							|  |  |  |         > | 
					
						
							|  |  |  |     > = {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private readonly _poiDatabase: OsmPoiDatabase | 
					
						
							|  |  |  |     private readonly _maxAge: number | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 14:22:16 +01:00
										 |  |  |     constructor(poiDatabase: OsmPoiDatabase, maxAge: number) { | 
					
						
							|  |  |  |         this._poiDatabase = poiDatabase | 
					
						
							|  |  |  |         this._maxAge = maxAge | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  |     public async getCount( | 
					
						
							|  |  |  |         layer: string, | 
					
						
							|  |  |  |         tileId: number | 
					
						
							|  |  |  |     ): Promise<{ count: number; lat: number; lon: number }> { | 
					
						
							| 
									
										
										
										
											2024-02-19 14:22:16 +01:00
										 |  |  |         const cachedEntry = this._cache[layer]?.[tileId] | 
					
						
							|  |  |  |         if (cachedEntry) { | 
					
						
							|  |  |  |             const age = (new Date().getTime() - cachedEntry.date.getTime()) / 1000 | 
					
						
							|  |  |  |             if (age < this._maxAge) { | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  |                 return cachedEntry.entry | 
					
						
							| 
									
										
										
										
											2024-02-19 14:22:16 +01:00
										 |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         const bbox = Tiles.tile_bounds_lon_lat(...Tiles.tile_from_index(tileId)) | 
					
						
							|  |  |  |         const count = await this._poiDatabase.getCount(layer, bbox) | 
					
						
							|  |  |  |         if (!this._cache[layer]) { | 
					
						
							|  |  |  |             this._cache[layer] = {} | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  |         this._cache[layer][tileId] = { entry: count, date: new Date() } | 
					
						
							| 
									
										
										
										
											2024-02-19 14:22:16 +01:00
										 |  |  |         return count | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  | class Server { | 
					
						
							|  |  |  |     constructor( | 
					
						
							|  |  |  |         port: number, | 
					
						
							| 
									
										
										
										
											2024-02-19 13:57:08 +01:00
										 |  |  |         options: { | 
					
						
							|  |  |  |             ignorePathPrefix?: string[] | 
					
						
							|  |  |  |         }, | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |         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) | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2024-02-19 13:57:08 +01:00
										 |  |  |                 if (options?.ignorePathPrefix) { | 
					
						
							|  |  |  |                     for (const toIgnore of options.ignorePathPrefix) { | 
					
						
							|  |  |  |                         if (path.startsWith(toIgnore)) { | 
					
						
							|  |  |  |                             path = path.substring(toIgnore.length + 1) | 
					
						
							|  |  |  |                             break | 
					
						
							|  |  |  |                         } | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |                 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(", ") | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 14:00:16 +01:00
										 |  |  | const connectionString = "postgresql://user:password@localhost:5444/osm-poi" | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  | const tcs = new OsmPoiDatabase(connectionString) | 
					
						
							| 
									
										
										
										
											2024-02-19 14:22:16 +01:00
										 |  |  | const withCache = new CachedSqlCount(tcs, 60 * 60 * 24) | 
					
						
							|  |  |  | new Server(2345, { ignorePathPrefix: ["summary"] }, [ | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-02-19 13:57:08 +01:00
										 |  |  |         mustMatch: "status.json", | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |         mimetype: "application/json", | 
					
						
							|  |  |  |         handle: async (path: string) => { | 
					
						
							|  |  |  |             const layers = await tcs.getLayers() | 
					
						
							|  |  |  |             const meta = await tcs.getMeta() | 
					
						
							| 
									
										
										
										
											2024-02-18 17:41:38 +01:00
										 |  |  |             return JSON.stringify({ meta, layers: Array.from(layers) }) | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |         }, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-02-19 13:57:08 +01:00
										 |  |  |         mustMatch: /[a-zA-Z0-9+_-]+\/[0-9]+\/[0-9]+\/[0-9]+\.json/, | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |         mimetype: "application/json", // "application/vnd.geo+json",
 | 
					
						
							|  |  |  |         async handle(path) { | 
					
						
							|  |  |  |             const [layers, z, x, y] = path.split(".")[0].split("/") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             let sum = 0 | 
					
						
							|  |  |  |             let properties: Record<string, number> = {} | 
					
						
							| 
									
										
										
										
											2024-02-18 15:59:28 +01:00
										 |  |  |             const availableLayers = await tcs.getLayers() | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  |             let latSum = 0 | 
					
						
							|  |  |  |             let lonSum = 0 | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |             for (const layer of layers.split("+")) { | 
					
						
							| 
									
										
										
										
											2024-02-18 15:59:28 +01:00
										 |  |  |                 if (!availableLayers.has(layer)) { | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2024-02-19 14:22:16 +01:00
										 |  |  |                 const count = await withCache.getCount( | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |                     layer, | 
					
						
							| 
									
										
										
										
											2024-02-19 14:22:16 +01:00
										 |  |  |                     Tiles.tile_index(Number(z), Number(x), Number(y)) | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |                 ) | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 latSum += count.lat * count.count | 
					
						
							|  |  |  |                 lonSum += count.lon * count.count | 
					
						
							|  |  |  |                 properties[layer] = count.count | 
					
						
							|  |  |  |                 sum += count.count | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 15:06:54 +01:00
										 |  |  |             properties["lon"] = lonSum / sum | 
					
						
							|  |  |  |             properties["lat"] = latSum / sum | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-15 17:39:59 +01:00
										 |  |  |             return JSON.stringify({ ...properties, total: sum }) | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  | ]) | 
					
						
							|  |  |  | console.log( | 
					
						
							|  |  |  |     ">>>", | 
					
						
							|  |  |  |     await tcs.getCount("drinking_water", [ | 
					
						
							|  |  |  |         [3.194358020772171, 51.228073636083394], | 
					
						
							|  |  |  |         [3.2839964396059145, 51.172701162680994], | 
					
						
							|  |  |  |     ]) | 
					
						
							|  |  |  | ) |