Skip to content

Commit

Permalink
Add mlt-vector-tile-js (#212)
Browse files Browse the repository at this point in the history
This PR adds a way to wrap MLT tiles in a similar API to
[mapbox/vector-tile-js](https://github.com/mapbox/vector-tile-js/),
which makes it possible to use MLTs in MapLibre GL JS. Eventually we
would expect this to be a separate package but since this is
experimental and the MLT API is changing we think it's reasonable to
incubate this here for now.
  • Loading branch information
ebrelsford authored Jun 28, 2024
1 parent 7436314 commit 97af75b
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 100 deletions.
2 changes: 1 addition & 1 deletion js/jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"transform": {
"^.+\\.ts": "ts-jest"
},
"testRegex": "test/unit/(.*|(\\.|/)(test|spec))\\.(js|ts)$",
"testRegex": "test/unit/.*.spec\\.(js|ts)$",
"moduleFileExtensions": ["ts", "js", "json", "node"],
"workerThreads": true
}
100 changes: 1 addition & 99 deletions js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions js/src/mlt-vector-tile-js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# mlt-vector-tile-js

This directory contains an API that is intended to mirror [mapbox/vector-tile-js](https://github.com/mapbox/vector-tile-js/) for MLTs to make it easier to use MLTs where the vector-tile-js API is expected (such as MapLibre GL JS).
16 changes: 16 additions & 0 deletions js/src/mlt-vector-tile-js/VectorTile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MltDecoder } from '../../src/decoder/MltDecoder';
import { VectorTileLayer } from './VectorTileLayer';

class VectorTile {
layers: { [_: string]: VectorTileLayer } = {};

constructor(data: Uint8Array, featureTables: any) {
const decoded = MltDecoder.decodeMlTile(data, featureTables);

for (const layerName of Object.keys(decoded.layers)) {
this.layers[layerName] = new VectorTileLayer(decoded.layers[layerName]);
}
}
}

export { VectorTile };
38 changes: 38 additions & 0 deletions js/src/mlt-vector-tile-js/VectorTileFeature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Point = require("@mapbox/point-geometry");

class VectorTileFeature {
properties: { [key: string]: any } = {};
extent: number;
type: 0|1|2|3 = 0;
id: number;

_raw: any;

constructor(feature) {
this.properties = feature.properties;
this.extent = feature.extent;
this._raw = feature;
if (feature.id !== null) {
this.id = Number(feature.id);
}
}

loadGeometry(): Point[][] {
// TODO: optimize to avoid needing this deep copy
const newGeometry = [];
const oldGeometry = this._raw.loadGeometry();
for (let i = 0; i < oldGeometry.length; i++) {
newGeometry[i] = [];
for (let j = 0; j < oldGeometry[i].length; j++) {
newGeometry[i][j] = new Point(oldGeometry[i][j].x, oldGeometry[i][j].y);
}
}
return newGeometry;
}

toGeoJSON(x: Number, y: Number, z: Number): any {
return this._raw.toGeoJSON(x, y, z);
}
}

export { VectorTileFeature };
23 changes: 23 additions & 0 deletions js/src/mlt-vector-tile-js/VectorTileLayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Layer } from '../data/Layer';
import { VectorTileFeature } from './VectorTileFeature';

class VectorTileLayer {
version: number;
name: string | null;
extent: number;
length: number = 0;

_raw: Layer;

constructor(layer: Layer) {
this.name = layer.name;
this._raw = layer;
this.length = layer.features.length;
}

feature(i: number): VectorTileFeature {
return new VectorTileFeature(this._raw.features[i]);
}
}

export { VectorTileLayer };
3 changes: 3 additions & 0 deletions js/src/mlt-vector-tile-js/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { VectorTile } from './VectorTile';
export { VectorTileLayer } from './VectorTileLayer';
export { VectorTileFeature } from './VectorTileFeature';
14 changes: 14 additions & 0 deletions js/test/unit/mlt-vector-tile-js/LoadMLTTile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as fs from 'fs';

import { MltDecoder, TileSetMetadata } from '../../../src/index';
import * as vt from '../../../src/mlt-vector-tile-js/index';

const loadTile = (tilePath, metadataPath) : vt.VectorTile => {
const data : Buffer = fs.readFileSync(tilePath);
const metadata : Buffer = fs.readFileSync(metadataPath);
const tilesetMetadata = TileSetMetadata.fromBinary(metadata);
const tile : vt.VectorTile = new vt.VectorTile(data, tilesetMetadata);
return tile;
};

export default loadTile;
11 changes: 11 additions & 0 deletions js/test/unit/mlt-vector-tile-js/LoadMVTTile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as fs from 'fs';

import { VectorTile } from '@mapbox/vector-tile';
import Protobuf from 'pbf';

const loadTile = (tilePath) => {
const data : Buffer = fs.readFileSync(tilePath);
return new VectorTile(new Protobuf(data));
};

export default loadTile;
38 changes: 38 additions & 0 deletions js/test/unit/mlt-vector-tile-js/VectorTile.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as fs from 'fs';

import type { VectorTile as MvtVectorTile } from '@mapbox/vector-tile';
import * as vt from '../../../src/mlt-vector-tile-js/index';
import loadTile from './LoadMLTTile';

describe("VectorTile", () => {
it("should have all layers", () => {
const tile: vt.VectorTile = loadTile('../test/expected/bing/4-13-6.mlt', '../test/expected/bing/4-13-6.mlt.meta.pbf');

expect(Object.keys(tile.layers)).toEqual([
"water_feature",
"road",
"land_cover_grass",
"country_region",
"land_cover_forest",
"road_hd",
"vector_background",
"populated_place",
"admin_division1",
]);
})

it("should return valid GeoJSON for a feature", () => {
const tile: vt.VectorTile = loadTile('../test/expected/bing/4-13-6.mlt', '../test/expected/bing/4-13-6.mlt.meta.pbf');

const geojson = tile.layers['road'].feature(0).toGeoJSON(13, 6, 4);

expect(geojson.type).toEqual('Feature');
expect(geojson.geometry.type).toEqual('MultiLineString');
})

it("should be equivalent to MVT VectorTile", () => {
const tile: vt.VectorTile = loadTile('../test/expected/bing/4-13-6.mlt', '../test/expected/bing/4-13-6.mlt.meta.pbf');

const mvtType : MvtVectorTile = tile;
});
});
59 changes: 59 additions & 0 deletions js/test/unit/mlt-vector-tile-js/VectorTileFeature.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as fs from 'fs';

import { MltDecoder, TileSetMetadata } from '../../../src/index';
import * as vt from '../../../src/mlt-vector-tile-js/index';
import type { VectorTileFeature as MvtVectorTileFeature } from '@mapbox/vector-tile';
import loadTile from './LoadMLTTile';
import { default as loadMvtTile } from './LoadMVTTile';

describe("VectorTileFeature", () => {
it("should be assignable to MVT VectorTileFeature", () => {
const tile: vt.VectorTile = loadTile('../test/expected/bing/4-13-6.mlt', '../test/expected/bing/4-13-6.mlt.meta.pbf');
const feature : vt.VectorTileFeature = tile.layers['road'].feature(0);
const mvtFeature : MvtVectorTileFeature = feature;
});

it("should return valid GeoJSON for a feature", () => {
const tile: vt.VectorTile = loadTile('../test/expected/bing/4-13-6.mlt', '../test/expected/bing/4-13-6.mlt.meta.pbf');

const feature = tile.layers['road'].feature(0);
const geojsonFeature: any = feature.toGeoJSON(13, 6, 4);
const geometry: any = geojsonFeature.geometry;
expect(geometry.coordinates[0][0][0]).not.toBe(undefined);
});

it("should load geometry", () => {
const tile: vt.VectorTile = loadTile('../test/expected/bing/4-13-6.mlt', '../test/expected/bing/4-13-6.mlt.meta.pbf');
const feature = tile.layers['road'].feature(0);
expect(feature.loadGeometry()[0][0].x).not.toBe(undefined);
});

it("should have a valid extent", () => {
const tile: vt.VectorTile = loadTile('../test/expected/bing/4-13-6.mlt', '../test/expected/bing/4-13-6.mlt.meta.pbf');
expect(tile.layers['road'].feature(0).extent).not.toBe(undefined);
});

it("should have same loadGeometry output as MVT", () => {
const tile: vt.VectorTile = loadTile('../test/expected/bing/4-13-6.mlt', '../test/expected/bing/4-13-6.mlt.meta.pbf');

const mvtTile = loadMvtTile('../test/fixtures/bing/4-13-6.mvt');

for (let i = 0; i < tile.layers['country_region'].length; i++) {
const mltFeature = tile.layers['country_region'].feature(i);
const mvtFeature = mvtTile.layers['country_region'].feature(i);
expect(mltFeature.loadGeometry()).toEqual(mvtFeature.loadGeometry());
}
});

it.skip("should have same type output as MVT", () => {
const tile: vt.VectorTile = loadTile('../test/expected/bing/4-13-6.mlt', '../test/expected/bing/4-13-6.mlt.meta.pbf');

const mvtTile = loadMvtTile('../test/fixtures/bing/4-13-6.mvt');

for (let i = 0; i < tile.layers['land_cover_grass'].length; i++) {
const mltFeature = tile.layers['land_cover_grass'].feature(i);
const mvtFeature = mvtTile.layers['land_cover_grass'].feature(i);
expect(mltFeature.type).toEqual(mvtFeature.type);
}
});
});
Loading

0 comments on commit 97af75b

Please sign in to comment.