From d079f8379b2c4203efe4e65e6234c73f08b9c46a Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 29 Sep 2024 01:04:31 +0200 Subject: [PATCH] Chore: move code to external package --- package-lock.json | 20 +- package.json | 2 +- src/Logic/FeatureSource/Sources/MvtSource.ts | 405 +------------------ 3 files changed, 24 insertions(+), 403 deletions(-) diff --git a/package-lock.json b/package-lock.json index 244e83b8c..cea1831a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "maplibre-gl": "^4.1.1", "marked": "^12.0.2", "monaco-editor": "^0.46.0", + "mvt-to-geojson": "^0.0.2", "name-suggestion-index": "^6.0.20240422", "npm": "^10.7.0", "opening_hours": "^3.6.0", @@ -66,7 +67,6 @@ "panoramax-js": "^0.1.7", "panzoom": "^9.4.3", "papaparse": "^5.3.1", - "pbf": "^3.2.1", "pg": "^8.11.3", "pic4carto": "^2.1.15", "pmtiles": "^3.0.5", @@ -13145,6 +13145,15 @@ "version": "1.0.0", "license": "MIT" }, + "node_modules/mvt-to-geojson": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/mvt-to-geojson/-/mvt-to-geojson-0.0.2.tgz", + "integrity": "sha512-DLi1Hgowqy6ygOUIuOhtI5RLdph9VpQSP6Ct3qbOY/y0dSTBjTvpIXgbK3J0XSO9feBRrhjcQAMkzOVD/KkuDA==", + "dependencies": { + "@types/geojson": "^7946.0.14", + "pbf": "^3.2.1" + } + }, "node_modules/n3": { "version": "1.17.3", "license": "MIT", @@ -30231,6 +30240,15 @@ "murmurhash-js": { "version": "1.0.0" }, + "mvt-to-geojson": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/mvt-to-geojson/-/mvt-to-geojson-0.0.2.tgz", + "integrity": "sha512-DLi1Hgowqy6ygOUIuOhtI5RLdph9VpQSP6Ct3qbOY/y0dSTBjTvpIXgbK3J0XSO9feBRrhjcQAMkzOVD/KkuDA==", + "requires": { + "@types/geojson": "^7946.0.14", + "pbf": "^3.2.1" + } + }, "n3": { "version": "1.17.3", "requires": { diff --git a/package.json b/package.json index 8182bd57f..2f161e592 100644 --- a/package.json +++ b/package.json @@ -200,6 +200,7 @@ "maplibre-gl": "^4.1.1", "marked": "^12.0.2", "monaco-editor": "^0.46.0", + "mvt-to-geojson": "^0.0.2", "name-suggestion-index": "^6.0.20240422", "npm": "^10.7.0", "opening_hours": "^3.6.0", @@ -208,7 +209,6 @@ "panoramax-js": "^0.1.7", "panzoom": "^9.4.3", "papaparse": "^5.3.1", - "pbf": "^3.2.1", "pg": "^8.11.3", "pic4carto": "^2.1.15", "pmtiles": "^3.0.5", diff --git a/src/Logic/FeatureSource/Sources/MvtSource.ts b/src/Logic/FeatureSource/Sources/MvtSource.ts index 1605ba177..b65595016 100644 --- a/src/Logic/FeatureSource/Sources/MvtSource.ts +++ b/src/Logic/FeatureSource/Sources/MvtSource.ts @@ -2,332 +2,8 @@ import { Feature as GeojsonFeature, Geometry } from "geojson" import { Store, UIEventSource } from "../../UIEventSource" import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource" -import Pbf from "pbf" +import { MvtToGeojson } from "mvt-to-geojson" -type Coords = [number, number][] - -class MvtFeatureBuilder { - private static readonly geom_types = ["Unknown", "Point", "LineString", "Polygon"] as const - private readonly _size: number - private readonly _x0: number - private readonly _y0: number - - constructor(extent: number, x: number, y: number, z: number) { - this._size = extent * Math.pow(2, z) - this._x0 = extent * x - this._y0 = extent * y - } - - private static signedArea(ring: Coords): number { - let sum = 0 - const len = ring.length - // J is basically (i - 1) % len - let j = len - 1 - let p1 - let p2 - for (let i = 0; i < len; i++) { - p1 = ring[i] - p2 = ring[j] - sum += (p2.x - p1.x) * (p1.y + p2.y) - j = i - } - return sum - } - - /** - * - * const rings = [ [ [ 3.208361864089966, 51.186908820014736 ], [ 3.2084155082702637, 51.18689537073311 ], [ 3.208436965942383, 51.186888646090836 ], [ 3.2084155082702637, 51.18686174751187 ], [ 3.2084155082702637, 51.18685502286465 ], [ 3.2083725929260254, 51.18686847215807 ], [ 3.2083404064178467, 51.18687519680333 ], [ 3.208361864089966, 51.186908820014736 ] ] ] - * MvtFeatureBuilder.classifyRings(rings) // => [rings] - */ - private static classifyRings(rings: Coords[]): Coords[][] { - if (rings.length <= 0) { - throw "Now rings in polygon found" - } - if (rings.length == 1) { - return [rings] - } - - const polygons: Coords[][] = [] - let currentPolygon: Coords[] - - for (let i = 0; i < rings.length; i++) { - let ring = rings[i] - const area = this.signedArea(ring) - if (area === 0) { - // Weird, degenerate ring - continue - } - const ccw = area < 0 - - if (ccw === area < 0) { - if (currentPolygon) { - polygons.push(currentPolygon) - } - currentPolygon = [ring] - } else { - currentPolygon.push(ring) - } - } - if (currentPolygon) { - polygons.push(currentPolygon) - } - - return polygons - } - - public toGeoJson(geometry: number[], typeIndex: 1 | 2 | 3, properties: any): GeojsonFeature { - let coords: Coords[] = this.encodeGeometry(geometry) - let classified = undefined - switch (typeIndex) { - case 1: - const points = [] - for (let i = 0; i < coords.length; i++) { - points[i] = coords[i][0] - } - coords = points - this.project(coords) - break - - case 2: - for (let i = 0; i < coords.length; i++) { - this.project(coords[i]) - } - break - - case 3: - classified = MvtFeatureBuilder.classifyRings(coords) - for (let i = 0; i < classified.length; i++) { - for (let j = 0; j < classified[i].length; j++) { - this.project(classified[i][j]) - } - } - break - } - - let type: string = MvtFeatureBuilder.geom_types[typeIndex] - let polygonCoords: Coords | Coords[] | Coords[][] - if (coords.length === 1) { - polygonCoords = (classified ?? coords)[0] - } else { - polygonCoords = classified ?? coords - type = "Multi" + type - } - - return { - type: "Feature", - geometry: { - type: type, - coordinates: polygonCoords, - }, - properties, - } - } - - /** - * - * const geometry = [9,233,8704,130,438,1455,270,653,248,423,368,493,362,381,330,267,408,301,406,221,402,157,1078,429,1002,449,1036,577,800,545,1586,1165,164,79,40] - * const builder = new MvtFeatureBuilder(4096, 66705, 43755, 17) - * const expected = [[3.2106759399175644,51.213658395282124],[3.2108227908611298,51.21396418776169],[3.2109133154153824,51.21410154168976],[3.210996463894844,51.214190590500664],[3.211119845509529,51.214294340548975],[3.211241215467453,51.2143745681588],[3.2113518565893173,51.21443085341426],[3.211488649249077,51.21449427925393],[3.2116247713565826,51.214540903490956],[3.211759552359581,51.21457408647774],[3.2121209800243378,51.214664394485254],[3.212456926703453,51.21475890267553],[3.2128042727708817,51.214880292910834],[3.213072493672371,51.214994962285544],[3.2136042416095734,51.21523984134939],[3.2136592268943787,51.21525664260963],[3.213672637939453,51.21525664260963]] - * builder.project(builder.encodeGeometry(geometry)[0]) // => expected - * @param geometry - * @private - */ - private encodeGeometry(geometry: number[]): Coords[] { - let cX = 0 - let cY = 0 - const coordss: Coords[] = [] - let currentRing: Coords = [] - for (let i = 0; i < geometry.length; i++) { - const commandInteger = geometry[i] - const commandId = commandInteger & 0x7 - const commandCount = commandInteger >> 3 - /* - Command Id Parameters Parameter Count - MoveTo 1 dX, dY 2 - LineTo 2 dX, dY 2 - ClosePath 7 No parameters 0 - */ - if (commandId === 1) { - // MoveTo means: we start a new ring - if (currentRing.length !== 0) { - coordss.push(currentRing) - currentRing = [] - } - } - if (commandId === 1 || commandId === 2) { - for (let j = 0; j < commandCount; j++) { - const dx = geometry[i + j * 2 + 1] - cX += (dx >> 1) ^ -(dx & 1) - const dy = geometry[i + j * 2 + 2] - cY += (dy >> 1) ^ -(dy & 1) - currentRing.push([cX, cY]) - } - i += commandCount * 2 - } - if (commandId === 7) { - if (currentRing.length === 0) { - console.error( - "Invalid MVT file: got a 'closePath', but the currentRing is empty. Full command:", - commandInteger - ) - } else { - currentRing.push([...currentRing[0]]) - } - i++ - } - } - if (currentRing.length > 0) { - coordss.push(currentRing) - } - return coordss - } - - /** - * Inline replacement of the location by projecting - * @param line the line which will be rewritten inline - * @return line - */ - private project(line: Coords) { - const y0 = this._y0 - const x0 = this._x0 - const size = this._size - for (let i = 0; i < line.length; i++) { - let p = line[i] - let y2 = 180 - ((p[1] + y0) * 360) / size - line[i] = [ - ((p[0] + x0) * 360) / size - 180, - (360 / Math.PI) * Math.atan(Math.exp((y2 * Math.PI) / 180)) - 90, - ] - } - return line - } -} - -class Layer { - public static read(pbf, end) { - return pbf.readFields( - Layer._readField, - { version: 0, name: "", features: [], keys: [], values: [], extent: 0 }, - end - ) - } - - static _readField(tag, obj, pbf) { - if (tag === 15) obj.version = pbf.readVarint() - else if (tag === 1) obj.name = pbf.readString() - else if (tag === 2) obj.features.push(Feature.read(pbf, pbf.readVarint() + pbf.pos)) - else if (tag === 3) obj.keys.push(pbf.readString()) - else if (tag === 4) obj.values.push(Value.read(pbf, pbf.readVarint() + pbf.pos)) - else if (tag === 5) obj.extent = pbf.readVarint() - } - - public static write(obj, pbf) { - if (obj.version) pbf.writeVarintField(15, obj.version) - if (obj.name) pbf.writeStringField(1, obj.name) - if (obj.features) - for (var i = 0; i < obj.features.length; i++) - pbf.writeMessage(2, Feature.write, obj.features[i]) - if (obj.keys) for (i = 0; i < obj.keys.length; i++) pbf.writeStringField(3, obj.keys[i]) - if (obj.values) - for (i = 0; i < obj.values.length; i++) pbf.writeMessage(4, Value.write, obj.values[i]) - if (obj.extent) pbf.writeVarintField(5, obj.extent) - } -} - -class Feature { - static read(pbf, end) { - return pbf.readFields(Feature._readField, { id: 0, tags: [], type: 0, geometry: [] }, end) - } - - static _readField(tag, obj, pbf) { - if (tag === 1) obj.id = pbf.readVarint() - else if (tag === 2) pbf.readPackedVarint(obj.tags) - else if (tag === 3) obj.type = pbf.readVarint() - else if (tag === 4) pbf.readPackedVarint(obj.geometry) - } - - public static write(obj, pbf) { - if (obj.id) pbf.writeVarintField(1, obj.id) - if (obj.tags) pbf.writePackedVarint(2, obj.tags) - if (obj.type) pbf.writeVarintField(3, obj.type) - if (obj.geometry) pbf.writePackedVarint(4, obj.geometry) - } -} - -class Value { - public static read(pbf, end) { - return pbf.readFields( - Value._readField, - { - string_value: "", - float_value: 0, - double_value: 0, - int_value: 0, - uint_value: 0, - sint_value: 0, - bool_value: false, - }, - end - ) - } - - static _readField = function (tag, obj, pbf) { - if (tag === 1) obj.string_value = pbf.readString() - else if (tag === 2) obj.float_value = pbf.readFloat() - else if (tag === 3) obj.double_value = pbf.readDouble() - else if (tag === 4) obj.int_value = pbf.readVarint(true) - else if (tag === 5) obj.uint_value = pbf.readVarint() - else if (tag === 6) obj.sint_value = pbf.readSVarint() - else if (tag === 7) obj.bool_value = pbf.readBoolean() - } - - public static write(obj, pbf) { - if (obj.string_value) pbf.writeStringField(1, obj.string_value) - if (obj.float_value) pbf.writeFloatField(2, obj.float_value) - if (obj.double_value) pbf.writeDoubleField(3, obj.double_value) - if (obj.int_value) pbf.writeVarintField(4, obj.int_value) - if (obj.uint_value) pbf.writeVarintField(5, obj.uint_value) - if (obj.sint_value) pbf.writeSVarintField(6, obj.sint_value) - if (obj.bool_value) pbf.writeBooleanField(7, obj.bool_value) - } -} - -class Tile { - // code generated by pbf v3.2.1 - - static GeomType = { - UNKNOWN: { - value: 0, - options: {}, - }, - POINT: { - value: 1, - options: {}, - }, - LINESTRING: { - value: 2, - options: {}, - }, - POLYGON: { - value: 3, - options: {}, - }, - } - - public static read(pbf, end) { - return pbf.readFields(Tile._readField, { layers: [] }, end) - } - - static _readField(tag, obj, pbf) { - if (tag === 3) obj.layers.push(Layer.read(pbf, pbf.readVarint() + pbf.pos)) - } - - static write(obj, pbf) { - if (obj.layers) - for (var i = 0; i < obj.layers.length; i++) - pbf.writeMessage(3, Layer.write, obj.layers[i]) - } -} export default class MvtSource implements FeatureSourceForTile, UpdatableFeatureSource { public readonly features: Store[]> @@ -352,7 +28,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature y: number, z: number, layerName?: string, - isActive?: Store + isActive?: Store, ) { this._url = url this._layerName = layerName @@ -367,7 +43,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature } return fs }, - [isActive] + [isActive], ) } @@ -378,39 +54,6 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature await this.currentlyRunning } - private getValue(v: { - // Exactly one of these values must be present in a valid message - string_value?: string - float_value?: number - double_value?: number - int_value?: number - uint_value?: number - sint_value?: number - bool_value?: boolean - }): string | number | undefined | boolean { - if (v.string_value !== "") { - return v.string_value - } - if (v.double_value !== 0) { - return v.double_value - } - if (v.float_value !== 0) { - return v.float_value - } - if (v.int_value !== 0) { - return v.int_value - } - if (v.uint_value !== 0) { - return v.uint_value - } - if (v.sint_value !== 0) { - return v.sint_value - } - if (v.bool_value !== false) { - return v.bool_value - } - return undefined - } private async download(): Promise { try { @@ -420,52 +63,12 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature return } const buffer = await result.arrayBuffer() - const data = Tile.read(new Pbf(buffer), undefined) - const layers = data.layers - let layer = data.layers[0] - if (layers.length > 1) { - if (!this._layerName) { - throw "Multiple layers in the downloaded tile, but no layername is given to choose from" - } - layer = layers.find((l) => l.name === this._layerName) - } - if (!layer) { - return - } - const builder = new MvtFeatureBuilder(layer.extent, this.x, this.y, this.z) - const features: GeojsonFeature[] = [] - - for (const feature of layer.features) { - const properties = this.inflateProperties(feature.tags, layer.keys, layer.values) - features.push(builder.toGeoJson(feature.geometry, feature.type, properties)) - } + const features = await MvtToGeojson.fromBuffer(buffer, this.x, this.y, this.z) this._features.setData(features) } catch (e) { console.error("Could not download MVT " + this._url + " tile due to", e) } } - private inflateProperties(tags: number[], keys: string[], values: { string_value: string }[]) { - const properties = {} - for (let i = 0; i < tags.length; i += 2) { - properties[keys[tags[i]]] = this.getValue(values[tags[i + 1]]) - } - let type: string - switch (properties["osm_type"]) { - case "N": - type = "node" - break - case "W": - type = "way" - break - case "R": - type = "relation" - break - } - properties["id"] = type + "/" + properties["osm_id"] - delete properties["osm_id"] - delete properties["osm_type"] - return properties - } }