diff --git a/modules/mvt/src/index.ts b/modules/mvt/src/index.ts index 58cbcc40e8..c2be05fcb6 100644 --- a/modules/mvt/src/index.ts +++ b/modules/mvt/src/index.ts @@ -13,6 +13,10 @@ export type {TileJSON} from './lib/parse-tilejson'; export {MVTLoader, MVTWorkerLoader} from './mvt-loader'; export type {MVTLoaderOptions} from './mvt-loader'; +// MVTWriter + +export {MVTWriter} from './mvt-writer'; + // MVTSource export {MVTSource} from './mvt-source'; diff --git a/modules/mvt/src/lib/encode-mvt.ts b/modules/mvt/src/lib/encode-mvt.ts new file mode 100644 index 0000000000..5a8d7bcfc2 --- /dev/null +++ b/modules/mvt/src/lib/encode-mvt.ts @@ -0,0 +1,9 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {fromGeojson} from './mapbox-vt-pbf/to-vector-tile'; + +export function encodeMVT(data, options) { + return fromGeojson(data, options); +} diff --git a/modules/mvt/src/lib/mapbox-vt-pbf/geojson-to-vt.ts.disabled b/modules/mvt/src/lib/mapbox-vt-pbf/geojson-to-vt.ts.disabled new file mode 100644 index 0000000000..acf8ea0e44 --- /dev/null +++ b/modules/mvt/src/lib/mapbox-vt-pbf/geojson-to-vt.ts.disabled @@ -0,0 +1,76 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright vis.gl contributors + +// Forked from https://github.com/mapbox/vt-pbf under MIT License Copyright (c) 2015 Anand Thakker + +import VectorTileFeature from "../mapbox-vector-tile-js/vector-tile-feature"; + +export function geojsonFeatureToVt(feature) { + const tags = null; + + const vtFeature = { + type: featureTypeToVt(feature.type), + geometry: geometryToVt(feature.geometry), + tags + }; + + if (feature.id !== null) { + vtFeature.id = feature.id; + } + + return VectorTileFeature; +} + +function featureTypeToVt(type) { + switch (type) { + case 'Polygon': + case 'MultiPolygon': + return 3; + case 'LineString': + case 'MultiLineString': + return 2; + default : + return 1; + } +} + +function geometryToVt(geometry, type) { + const simplified = []; + + // tile.minX = Math.min(tile.minX, feature.minX); + // tile.minY = Math.min(tile.minY, feature.minY); + // tile.maxX = Math.max(tile.maxX, feature.maxX); + // tile.maxY = Math.max(tile.maxY, feature.maxY); + + switch (type) { + case 'Point': + case 'MultiPoint': + for (let i = 0; i < geometry.length; i += 3) { + simplified.push(geometry[i], geometry[i + 1]); + tile.numPoints++; + tile.numSimplified++; + } + break; + + case 'LineString': + addLine(simplified, geometry, tile, tolerance, false, false); + break; + + case 'MultiLineString': + case 'Polygon': + for (let i = 0; i < geometry.length; i++) { + addLine(simplified, geometry[i], tile, tolerance, type === 'Polygon', i === 0); + } + break; + + case 'MultiPolygon': + for (let k = 0; k < geometry.length; k++) { + const polygon = geometry[k]; + for (let i = 0; i < polygon.length; i++) { + addLine(simplified, polygon[i], tile, tolerance, true, i === 0); + } + } + break; + } +} diff --git a/modules/mvt/src/lib/mapbox-vt-pbf/geojson-wrapper.ts b/modules/mvt/src/lib/mapbox-vt-pbf/geojson-wrapper.ts new file mode 100644 index 0000000000..12160cc27e --- /dev/null +++ b/modules/mvt/src/lib/mapbox-vt-pbf/geojson-wrapper.ts @@ -0,0 +1,89 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright vis.gl contributors + +// Forked from https://github.com/mapbox/vt-pbf under MIT License Copyright (c) 2015 Anand Thakker + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +// conform to vectortile api +export default class GeoJSONWrapper { + options; + features: any[]; + length: number; + + constructor(features, options = {}) { + this.options = options; + this.features = features; + this.length = features.length; + } + + feature(index) { + return new FeatureWrapper(this.features[index], this.options.extent); + } +} + +class FeatureWrapper { + id; + type; + rawGeometry: any; + properties; + extent; + geometry: Point[][] = []; + + constructor(feature, extent) { + this.id = typeof feature.id === 'number' ? feature.id : undefined; + this.type = feature.type; + this.rawGeometry = feature.type === 1 ? [feature.geometry] : feature.geometry; + this.properties = feature.tags; + this.extent = extent || 4096; + } + + loadGeometry() { + const rings = this.rawGeometry; + this.geometry = []; + + for (const ring of rings) { + const newRing: Point[] = []; + for (const coord of ring) { + newRing.push(new Point(coord[0], coord[1])); + } + this.geometry.push(newRing); + } + return this.geometry; + } + + bbox() { + if (!this.geometry) { + this.loadGeometry(); + } + + const rings = this.geometry; + let x1 = Infinity; + let x2 = -Infinity; + let y1 = Infinity; + let y2 = -Infinity; + + for (const ring of rings) { + for (const coord of ring) { + x1 = Math.min(x1, coord.x); + x2 = Math.max(x2, coord.x); + y1 = Math.min(y1, coord.y); + y2 = Math.max(y2, coord.y); + } + } + + return [x1, y1, x2, y2]; + } + + // toGeoJSON(x, y, z) { + // return VectorTileFeature.prototype.toGeoJSON.call(this, x, y, z); + // } +} diff --git a/modules/mvt/src/lib/mapbox-vt-pbf/to-vector-tile.ts b/modules/mvt/src/lib/mapbox-vt-pbf/to-vector-tile.ts new file mode 100644 index 0000000000..dd5018f721 --- /dev/null +++ b/modules/mvt/src/lib/mapbox-vt-pbf/to-vector-tile.ts @@ -0,0 +1,85 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright vis.gl contributors + +// Forked from https://github.com/mapbox/vt-pbf under MIT License Copyright (c) 2015 Anand Thakker + +import Pbf from 'pbf'; +import type {MVTTile} from '../mvt-pbf/mvt-types'; +import {writeMVT} from '../mvt-pbf/write-mvt-to-pbf'; +import GeoJSONWrapper from './geojson-wrapper'; + +/** + * Serialize a map of geojson layers + * loaders.gl addition + * + * @param {object | object[]} geojson + * @param {object} [options] - An object specifying the vector-tile specification version and extent that were used to create `layers`. + * @param {number} [options.extent=4096] - Extent of the vector tile + * @return {ArrayBuffer} uncompressed, pbf-serialized tile data + */ +export function fromGeojson(geojson, options) { + options = options || {}; + geojson = normalizeGeojson(geojson); + const layer = new GeoJSONWrapper(geojson.features); + + // @ts-expect-error + return fromVectorTileJs({layers: {geojsonLayer: layer}}); +} + +/** + * Serialize a vector-tile-js-created tile to pbf + * + * @param {object} tile + * @return {ArrayBuffer} uncompressed, pbf-serialized tile data + */ +export function fromVectorTileJs(tile: MVTTile) { + const pbf = new Pbf(); + writeMVT(tile, pbf); + const uint8Array = pbf.finish(); + // TODO - make sure no byteOffsets/byteLenghts are used? + return uint8Array.buffer.slice( + uint8Array.byteOffset, + uint8Array.byteOffset + uint8Array.byteLength + ); +} + +/** + * Serialized a geojson-vt-created tile to pbf. + * + * @param {object} vtLayers - An object mapping layer names to geojson-vt-created vector tile objects + * @param {object} [options] - An object specifying the vector-tile specification version and extent that were used to create `layers`. + * @param {number} [options.version=1] - Version of vector-tile spec used + * @param {number} [options.extent=4096] - Extent of the vector tile + * @return {ArrayBuffer} uncompressed, pbf-serialized tile data + * +export function fromGeojsonVt(vtLayers, options) { + options = options || {}; + const layers = {}; + for (const key in vtLayers) { + layers[key] = new GeoJSONWrapper(vtLayers[key].features, options); + layers[key].name = key; + layers[key].version = options.version; + layers[key].extent = options.extent; + } + return fromVectorTileJs({layers}); +} +*/ + +export function normalizeGeojson(geojson) { + // Array of features + if (Array.isArray(geojson)) { + return { + type: 'FeatureCollection', + features: geojson + }; + } + // A single feature + if (geojson.type !== 'FeatureCollection') { + return { + type: 'FeatureCollection', + features: [geojson] + }; + } + return geojson; +} diff --git a/modules/mvt/src/lib/pojo-parser/mvt-constants.ts b/modules/mvt/src/lib/mvt-pbf/mvt-constants.ts similarity index 100% rename from modules/mvt/src/lib/pojo-parser/mvt-constants.ts rename to modules/mvt/src/lib/mvt-pbf/mvt-constants.ts diff --git a/modules/mvt/src/lib/pojo-parser/mvt-types.ts b/modules/mvt/src/lib/mvt-pbf/mvt-types.ts similarity index 100% rename from modules/mvt/src/lib/pojo-parser/mvt-types.ts rename to modules/mvt/src/lib/mvt-pbf/mvt-types.ts diff --git a/modules/mvt/src/lib/pojo-parser/parse-geometry-from-pbf.ts b/modules/mvt/src/lib/mvt-pbf/parse-geometry-from-pbf.ts similarity index 100% rename from modules/mvt/src/lib/pojo-parser/parse-geometry-from-pbf.ts rename to modules/mvt/src/lib/mvt-pbf/parse-geometry-from-pbf.ts diff --git a/modules/mvt/src/lib/pojo-parser/parse-mvt-from-pbf.ts b/modules/mvt/src/lib/mvt-pbf/parse-mvt-from-pbf.ts similarity index 100% rename from modules/mvt/src/lib/pojo-parser/parse-mvt-from-pbf.ts rename to modules/mvt/src/lib/mvt-pbf/parse-mvt-from-pbf.ts diff --git a/modules/mvt/src/lib/mvt-pbf/write-mvt-to-pbf.ts b/modules/mvt/src/lib/mvt-pbf/write-mvt-to-pbf.ts new file mode 100644 index 0000000000..8134f59198 --- /dev/null +++ b/modules/mvt/src/lib/mvt-pbf/write-mvt-to-pbf.ts @@ -0,0 +1,150 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright vis.gl contributors + +// Forked from https://github.com/mapbox/vt-pbf under MIT License Copyright (c) 2015 Anand Thakker + +import Protobuf from 'pbf'; +import type {MVTTile, MVTLayer} from './mvt-types'; +import * as MVT from './mvt-constants'; + +export function writeMVT(tile: MVTTile, pbf?: Protobuf): void { + for (const key in tile.layers) { + pbf!.writeMessage(MVT.TileInfo.layers, writeLayer, tile.layers[key]); + } +} + +function writeLayer(layer: MVTLayer, pbf?: Protobuf): void { + pbf!.writeVarintField(MVT.LayerInfo.version, layer.version || 1); + pbf!.writeStringField(MVT.LayerInfo.name, layer.name || ''); + pbf!.writeVarintField(MVT.LayerInfo.extent, layer.extent || 4096); + + const context = { + keys: [], + values: [], + keycache: {}, + valuecache: {} + }; + + // TODO + // for (let i = 0; i < layer.length; i++) { + // context.feature = layer.feature(i); + // pbf!.writeMessage(2, writeFeature, context); + // } + + const keys = context.keys; + for (let i = 0; i < keys.length; i++) { + pbf!.writeStringField(3, keys[i]); + } + + const values = context.values; + for (let i = 0; i < values.length; i++) { + pbf!.writeMessage(4, writeValue, values[i]); + } +} + +// function writeFeature(context, pbf?: Protobuf): void { +// const feature = context.feature; + +// if (feature.id !== undefined) { +// pbf!.writeVarintField(1, feature.id); +// } + +// pbf!.writeMessage(2, writeProperties, context); +// pbf!.writeVarintField(3, feature.type); +// pbf!.writeMessage(4, writeGeometry, feature); +// } + +// function writeProperties(context, pbf?: Protobuf): void { +// const feature = context.feature; +// const keys = context.keys; +// const values = context.values; +// const keycache = context.keycache; +// const valuecache = context.valuecache; + +// for (const key in feature.properties) { +// let keyIndex = keycache[key]; +// if (typeof keyIndex === 'undefined') { +// keys.push(key); +// keyIndex = keys.length - 1; +// keycache[key] = keyIndex; +// } +// pbf!.writeVarint(keyIndex); + +// let value = feature.properties[key]; +// const type = typeof value; +// if (type !== 'string' && type !== 'boolean' && type !== 'number') { +// value = JSON.stringify(value); +// } +// const valueKey = `${type}:${value}`; +// let valueIndex = valuecache[valueKey]; +// if (typeof valueIndex === 'undefined') { +// values.push(value); +// valueIndex = values.length - 1; +// valuecache[valueKey] = valueIndex; +// } +// pbf!.writeVarint(valueIndex); +// } +// } + +// function command(cmd, length) { +// return (length << 3) + (cmd & 0x7); +// } + +// function zigzag(num) { +// return (num << 1) ^ (num >> 31); +// } + +// function writeGeometry(feature, pbf?: Protobuf): void { +// const geometry = feature.loadGeometry(); +// const type = feature.type; +// let x = 0; +// let y = 0; +// const rings = geometry.length; +// for (let r = 0; r < rings; r++) { +// const ring = geometry[r]; +// let count = 1; +// if (type === 1) { +// count = ring.length; +// } +// pbf!.writeVarint(command(1, count)); // moveto +// // do not write polygon closing path as lineto +// const lineCount = type === 3 ? ring.length - 1 : ring.length; +// for (let i = 0; i < lineCount; i++) { +// if (i === 1 && type !== 1) { +// pbf!.writeVarint(command(2, lineCount - 1)); // lineto +// } +// const dx = ring[i].x - x; +// const dy = ring[i].y - y; +// pbf!.writeVarint(zigzag(dx)); +// pbf!.writeVarint(zigzag(dy)); +// x += dx; +// y += dy; +// } +// if (type === 3) { +// pbf!.writeVarint(command(7, 1)); // closepath +// } +// } +// } + +function writeValue(value: unknown, pbf?: Protobuf): void { + switch (typeof value) { + case 'string': + pbf!.writeStringField(1, value); + break; + case 'boolean': + pbf!.writeBooleanField(7, value); + break; + case 'number': + if (value % 1 !== 0) { + pbf!.writeDoubleField(3, value); + } else if (value < 0) { + pbf!.writeSVarintField(6, value); + } else { + pbf!.writeVarintField(5, value); + } + break; + default: + // ignore + } +} diff --git a/modules/mvt/src/mvt-writer.ts b/modules/mvt/src/mvt-writer.ts new file mode 100644 index 0000000000..58168220d3 --- /dev/null +++ b/modules/mvt/src/mvt-writer.ts @@ -0,0 +1,28 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {WriterWithEncoder} from '@loaders.gl/loader-utils'; +import {MVTFormat} from './mvt-format'; +import {encodeMVT} from './lib/encode-mvt'; + +// __VERSION__ is injected by babel-plugin-version-inline +// @ts-ignore TS2304: Cannot find name '__VERSION__'. +const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest'; + +/** + * Writer for the Mapbox Vector Tile format + */ +export const MVTWriter = { + ...MVTFormat, + version: VERSION, + options: { + image: { + mimeType: 'image/png', + jpegQuality: null + } + }, + async encode(data, options) { + return encodeMVT(data, options); + } +} as const satisfies WriterWithEncoder; diff --git a/modules/mvt/test/data/mapbox-vt-pbf-fixtures/multi-points.json b/modules/mvt/test/data/mapbox-vt-pbf-fixtures/multi-points.json new file mode 100644 index 0000000000..350841b7b6 --- /dev/null +++ b/modules/mvt/test/data/mapbox-vt-pbf-fixtures/multi-points.json @@ -0,0 +1,17 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": 1, + "properties": {}, + "geometry": { + "type": "MultiPoint", + "coordinates": [ + [ -74.6685791015625, 48.4838455701099 ], + [ -75.12451171875, 47.669086647137576 ] + ] + } + } + ] +} \ No newline at end of file diff --git a/modules/mvt/test/data/mapbox-vt-pbf-fixtures/rectangle-1.0.0.pbf b/modules/mvt/test/data/mapbox-vt-pbf-fixtures/rectangle-1.0.0.pbf new file mode 100644 index 0000000000..3c260f4d80 Binary files /dev/null and b/modules/mvt/test/data/mapbox-vt-pbf-fixtures/rectangle-1.0.0.pbf differ diff --git a/modules/mvt/test/data/mapbox-vt-pbf-fixtures/rectangle.geojson b/modules/mvt/test/data/mapbox-vt-pbf-fixtures/rectangle.geojson new file mode 100644 index 0000000000..643701a3b9 --- /dev/null +++ b/modules/mvt/test/data/mapbox-vt-pbf-fixtures/rectangle.geojson @@ -0,0 +1,98 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": 123, + "properties": { + "mykey": "myvalue", + "myotherkey": 10 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -75.12451171875, + 47.669086647137576 + ], + [ + -75.12451171875, + 48.68370757165361 + ], + [ + -73.5369873046875, + 48.68370757165361 + ], + [ + -73.5369873046875, + 47.669086647137576 + ], + [ + -75.12451171875, + 47.669086647137576 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -74.6685791015625, + 48.4838455701099 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -74.1192626953125, + 48.480204398955145 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -74.37744140625, + 48.272225451004324 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -74.5751953125, + 48.17341248658084 + ], + [ + -74.57656860351562, + 48.04779189160941 + ], + [ + -74.19891357421875, + 48.049627977799595 + ], + [ + -74.19891357421875, + 48.1688331920297 + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/modules/mvt/test/index.ts b/modules/mvt/test/index.ts index 4cc96bfd96..03ba09c766 100644 --- a/modules/mvt/test/index.ts +++ b/modules/mvt/test/index.ts @@ -14,8 +14,11 @@ import './lib/vector-tiler/simplify-path.spec'; import './lib/utils/geometry-utils.spec'; import './tilejson-loader.spec'; + import './mvt-loader.spec'; +import './mvt-writer.spec'; import './mvt-source.spec'; + import './table-tile-source.spec'; // import './table-tile-source-full.spec'; // import './table-tile-source-multi-world.spec'; diff --git a/modules/mvt/test/lib/mapbox-vt-pbf/to-vector-tile.spec.js b/modules/mvt/test/lib/mapbox-vt-pbf/to-vector-tile.spec.js new file mode 100644 index 0000000000..88902bff51 --- /dev/null +++ b/modules/mvt/test/lib/mapbox-vt-pbf/to-vector-tile.spec.js @@ -0,0 +1,165 @@ +// @ts-nocheck +import test from 'tape-promise/tape'; +import {fetchFile} from '@loaders.gl/core'; +import Pbf from 'pbf'; +import VectorTile from '@loaders.gl/mvt/lib/mapbox-vector-tile-js/vector-tile'; +import {fromGeojsonVt} from '@loaders.gl/mvt/lib/mapbox-vt-pbf/to-vector-tile'; +import geojsonVt from 'geojson-vt'; +import GeoJsonEquality from 'geojson-equality'; + +const eq = new GeoJsonEquality({precision: 1}); + +test('property encoding: JSON.stringify non-primitive values', (t) => { + // Includes two properties with a common non-primitive value for + // https://github.com/mapbox/vt-pbf/issues/9 + const orig = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { + a: 'one', + b: 1, + c: {hello: 'world'}, + d: [1, 2, 3] + }, + geometry: { + type: 'Point', + coordinates: [0, 0] + } + }, + { + type: 'Feature', + properties: { + a: 'two', + b: 2, + c: {goodbye: 'planet'}, + d: {hello: 'world'} + }, + geometry: { + type: 'Point', + coordinates: [0, 0] + } + } + ] + }; + + const tileindex = geojsonVt(orig); + const tile = tileindex.getTile(1, 0, 0); + const buff = fromGeojsonVt({geojsonLayer: tile}); + + const vt = new VectorTile(new Pbf(buff)); + const layer = vt.layers.geojsonLayer; + + const first = layer.feature(0).properties; + const second = layer.feature(1).properties; + t.same(first.c, '{"hello":"world"}'); + t.same(first.d, '[1,2,3]'); + t.same(second.c, '{"goodbye":"planet"}'); + t.same(second.d, '{"hello":"world"}'); + t.end(); +}); + +test('number encoding https://github.com/mapbox/vt-pbf/pull/11', (t) => { + const orig = { + type: 'Feature', + properties: { + large_integer: 39953616224, // eslint-disable-line camelcase + non_integer: 331.75415 // eslint-disable-line camelcase + }, + geometry: { + type: 'Point', + coordinates: [0, 0] + } + }; + + const tileindex = geojsonVt(orig); + const tile = tileindex.getTile(1, 0, 0); + const buff = fromGeojsonVt({geojsonLayer: tile}); + const vt = new VectorTile(new Pbf(buff)); + const layer = vt.layers.geojsonLayer; + + const properties = layer.feature(0).properties; + t.equal(properties.large_integer, 39953616224); + t.equal(properties.non_integer, 331.75415); + t.end(); +}); + +test('id encoding', (t) => { + const orig = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + id: 123, + properties: {}, + geometry: { + type: 'Point', + coordinates: [0, 0] + } + }, + { + type: 'Feature', + id: 'invalid', + properties: {}, + geometry: { + type: 'Point', + coordinates: [0, 0] + } + }, + { + type: 'Feature', + // no id + properties: {}, + geometry: { + type: 'Point', + coordinates: [0, 0] + } + } + ] + }; + const tileindex = geojsonVt(orig); + const tile = tileindex.getTile(1, 0, 0); + const buff = fromGeojsonVt({geojsonLayer: tile}); + const vt = new VectorTile(new Pbf(buff)); + const layer = vt.layers.geojsonLayer; + t.same(layer.feature(0).id, 123); + t.notOk(layer.feature(1).id, 'Non-integer values should not be saved'); + t.notOk(layer.feature(2).id); + t.end(); +}); + +test('accept geojson-vt options https://github.com/mapbox/vt-pbf/pull/21', async (t) => { + const RECTANGLE_URL = '@loaders.gl/mvt/test/data/mapbox-vt-pbf-fixtures/rectangle.geojson'; + const response = await fetchFile(RECTANGLE_URL); + const orig = await response.json(); + + const version = 2; + const extent = 8192; + const tileindex = geojsonVt(orig, {extent}); + const tile = tileindex.getTile(1, 0, 0); + const options = {version, extent}; + const buff = fromGeojsonVt({geojsonLayer: tile}, options); + + const vt = new VectorTile(new Pbf(buff)); + const layer = vt.layers.geojsonLayer; + const features = []; + for (let i = 0; i < layer.length; i++) { + const feat = layer.feature(i).toGeoJSON(0, 0, 1); + features.push(feat); + } + + t.equal(layer.version, options.version, 'version should be equal'); + t.equal(layer.extent, options.extent, 'extent should be equal'); + + orig.features.forEach(function (expected) { + const actual = features.shift(); + + // TODO - this was added in loaders fork to make tests pass, investigate why it is needed + delete expected.id; + + t.ok(eq.compare(actual, expected)); + }); + + t.end(); +}); diff --git a/modules/mvt/test/lib/mapbox-vt-pbf/vector-tile-roundtrip.spec.js b/modules/mvt/test/lib/mapbox-vt-pbf/vector-tile-roundtrip.spec.js new file mode 100644 index 0000000000..92c6613de7 --- /dev/null +++ b/modules/mvt/test/lib/mapbox-vt-pbf/vector-tile-roundtrip.spec.js @@ -0,0 +1,130 @@ +// @ts-nocheck +import test from 'tape-promise/tape'; +import {isBrowser} from '@loaders.gl/loader-utils'; +import VectorTile from '@loaders.gl/mvt/lib/mapbox-vector-tile-js/vector-tile'; +import {fromGeojsonVt, fromVectorTileJs} from '@loaders.gl/mvt/lib/mapbox-vt-pbf/to-vector-tile'; +import Pbf from 'pbf'; +import geojsonVt from 'geojson-vt'; +import geojsonFixtures from '@mapbox/geojson-fixtures'; +import mvtf from '@mapbox/mvt-fixtures'; +import GeoJsonEquality from 'geojson-equality'; + +// Mock: vtvalidate library doesn't compile under Node 12 +const vtvalidate = { + isValid(buff, onValidationComplete) { + onValidationComplete(null, false); + } +}; + +const eq = new GeoJsonEquality({precision: 1}); + +test('geojson-vt', (t) => { + if (isBrowser) { + t.comment('Skipping as @mapbox/geojson-fixtures is only supported in Node.js'); + t.end(); + return; + } + + const geometryTypes = [ + 'polygon', + 'point', + 'multipoint', + 'multipolygon', + 'polygon', + 'multilinestring' + ]; + + const fixtures = geometryTypes.map(function (type) { + return { + name: type, + data: {type: 'Feature', properties: {}, geometry: geojsonFixtures.geometry[type]} + }; + }); + + fixtures.forEach(function (fixture) { + t.comment(`Testing ${fixture.name}`); + const tile = geojsonVt(fixture.data).getTile(0, 0, 0); + const buff = fromGeojsonVt({geojsonLayer: tile}); + vtvalidate.isValid(buff, (err, invalid) => { + t.error(err); + + t.ok(!invalid, invalid); + + // Compare roundtripped features with originals + const expected = + fixture.data.type === 'FeatureCollection' ? fixture.data.features : [fixture.data]; + const layer = new VectorTile(new Pbf(buff)).layers.geojsonLayer; + t.equal(layer.length, expected.length, `${expected.length} features`); + for (let i = 0; i < layer.length; i++) { + const actual = layer.feature(i).toGeoJSON(0, 0, 0); + t.ok(eq.compare(actual, expected[i]), `feature ${i}`); + } + t.end(); + }); + }); + + t.end(); +}); + +test('vector-tile-js', (t) => { + // See https://github.com/mapbox/mvt-fixtures/blob/master/FIXTURES.md for + // fixture descriptions + mvtf.each(function (fixture) { + // skip invalid tiles + if (!fixture.validity.v2) return; + + t.comment(`mvt-fixtures: ${fixture.id} ${fixture.description}`); + const original = new VectorTile(new Pbf(fixture.buffer)); + + if (fixture.id === '020') { + t.comment('Skipping test due to https://github.com/mapbox/vt-pbf/issues/30'); + t.end(); + return; + } + + if (fixture.id === '049' || fixture.id === '050') { + t.comment('Skipping test due to https://github.com/mapbox/vt-pbf/issues/31'); + t.end(); + return; + } + + const buff = fromVectorTileJs(original); + const roundtripped = new VectorTile(new Pbf(buff)); + + vtvalidate.isValid(buff, (err, invalid) => { + t.error(err); + + if (invalid && invalid === 'ClosePath command count is not 1') { + t.comment('Skipping test due to https://github.com/mapbox/vt-pbf/issues/28'); + t.end(); + return; + } + + // UNKOWN geometry type is valid in the spec, but vtvalidate considers + // it an error + if (fixture.id === '016' || fixture.id === '039') { + invalid = null; + } + + t.ok(!invalid, invalid); + + // Compare roundtripped features with originals + for (const name in original.layers) { + const originalLayer = original.layers[name]; + t.ok(roundtripped.layers[name], `layer ${name}`); + const roundtrippedLayer = roundtripped.layers[name]; + t.equal(roundtrippedLayer.length, originalLayer.length); + for (let i = 0; i < originalLayer.length; i++) { + const actual = roundtrippedLayer.feature(i); + const expected = originalLayer.feature(i); + + t.equal(actual.id, expected.id, 'id'); + t.equal(actual.type, expected.type, 'type'); + t.deepEqual(actual.properties, expected.properties, 'properties'); + t.deepEqual(actual.loadGeometry(), expected.loadGeometry(), 'geometry'); + } + } + }); + }); + t.end(); +}); diff --git a/modules/mvt/test/lib/parse-mvt-from-pbf.spec.ts b/modules/mvt/test/lib/parse-mvt-from-pbf.spec.ts index 96099b05e3..39481b3ad8 100644 --- a/modules/mvt/test/lib/parse-mvt-from-pbf.spec.ts +++ b/modules/mvt/test/lib/parse-mvt-from-pbf.spec.ts @@ -4,7 +4,7 @@ // import type {BinaryFeatureCollection} from '@loaders.gl/schema'; import test from 'tape-promise/tape'; -import {parseMVT} from '../../src/lib/pojo-parser/parse-mvt-from-pbf'; +import {parseMVT} from '../../src/lib/mvt-pbf/parse-mvt-from-pbf'; import {fetchFile} from '@loaders.gl/core'; // import {geojsonToBinary, binaryToGeojson} from '@loaders.gl/gis'; diff --git a/modules/mvt/test/mvt-writer.spec.ts b/modules/mvt/test/mvt-writer.spec.ts new file mode 100644 index 0000000000..e7ff1e49cc --- /dev/null +++ b/modules/mvt/test/mvt-writer.spec.ts @@ -0,0 +1,21 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import test from 'tape-promise/tape'; +// import {encode, fetchFile} from '@loaders.gl/core'; +import {MVTWriter} from '@loaders.gl/mvt'; + +// const RECTANGLE_URL = '@loaders.gl/mvt/test/data/mapbox-vt-pbf-fixtures/rectangle.geojson'; + +test('MVTWriter', async (t) => { + t.ok(MVTWriter, 'MVTWriter is defined'); + + // const response = await fetchFile(RECTANGLE_URL); + // const geojson = await response.json(); + + // const arrayBuffer = encode(geojson, MVTWriter); + // t.ok(arrayBuffer instanceof ArrayBuffer, 'MVTWriter encodes to ArrayBuffer'); + + t.end(); +});