diff --git a/.gitattributes b/.gitattributes index 1e57b2d6c0f..98313f24e46 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,7 @@ # Denote all files that are truly binary and should not be modified. *.png binary *.mvt binary +*.mlt binary *.pbf binary *.tif binary diff --git a/CHANGELOG.md b/CHANGELOG.md index 123a2d9f863..7de761930f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### ✨ Features and improvements - Improve performance of `GeoJSONSource#updateData` when called on small diffs ([#6562](https://github.com/maplibre/maplibre-gl-js/pull/6562)) +- Add support for MapLibre Tiles (MLT) by using `encoding: 'mlt'` in vector source definition ([#6570](https://github.com/maplibre/maplibre-gl-js/pull/6570)) - _...Add new stuff here..._ ### 🐞 Bug fixes @@ -13,10 +14,10 @@ - Add time control API (`setNow`, `restoreNow`, `isTimeFrozen`) for deterministic rendering, enabling frame-by-frame video export and deterministic testing ([6544](https://github.com/maplibre/maplibre-gl-js/pull/6544)) - Use styles `isHidden` logic in the worker by adding a new optional `roundMinZoom` parameter ([#6547](https://github.com/maplibre/maplibre-gl-js/pull/6547)) - Add `transformConstrain` callback to the `Map` options to override the transform's `constrain` with new type `TransformConstrainFunction`; refactor transform constructor options to a `TransformOptions` object ([#6484](https://github.com/maplibre/maplibre-gl-js/issues/6484)) -- Use timeControl.now() instead of browser.now() ([6573](https://github.com/maplibre/maplibre-gl-js/pull/6573)) +- Use timeControl.now() instead of browser.now() ([#6573](https://github.com/maplibre/maplibre-gl-js/pull/6573)) ### 🐞 Bug fixes -- Contextmenu events not blocked by scrolling ([#5683](https://github.com/maplibre/maplibre-gl-js/issues/5683) +- Contextmenu events not blocked by scrolling ([#5683](https://github.com/maplibre/maplibre-gl-js/issues/5683)) - Mousemove events are not blocked by scrolling ([#6302](https://github.com/maplibre/maplibre-gl-js/issues/6302)) - Dashed lines have blurry rounded caps ([#6554](https://github.com/maplibre/maplibre-gl-js/pull/6554)) - Preserve flyTo padding when prefers-reduced-motion is enabled ([#6576](https://github.com/maplibre/maplibre-gl-js/issues/6576)) diff --git a/package-lock.json b/package-lock.json index df920ab9f6b..950dffcbf92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@mapbox/vector-tile": "^2.0.4", "@mapbox/whoots-js": "^3.1.0", "@maplibre/maplibre-gl-style-spec": "^24.3.0", + "@maplibre/mlt": "^0.0.1-alpha.12", "@maplibre/vt-pbf": "^4.0.3", "@types/geojson": "^7946.0.16", "@types/geojson-vt": "3.2.5", @@ -2659,6 +2660,15 @@ "gl-style-validate": "dist/gl-style-validate.mjs" } }, + "node_modules/@maplibre/mlt": { + "version": "0.0.1-alpha.12", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-0.0.1-alpha.12.tgz", + "integrity": "sha512-eqynKLjQrZEwaaRYMFRTArqaXZayxvMM7GJqyDUsbioZ7toc7DQZN2/TBWNmYP0Oc+Aski5KQ8h0dhE9wlNrLQ==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, "node_modules/@maplibre/vt-pbf": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.0.3.tgz", diff --git a/package.json b/package.json index 897052072c0..e64ab2afb65 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@mapbox/vector-tile": "^2.0.4", "@mapbox/whoots-js": "^3.1.0", "@maplibre/maplibre-gl-style-spec": "^24.3.0", + "@maplibre/mlt": "^0.0.1-alpha.12", "@maplibre/vt-pbf": "^4.0.3", "@types/geojson": "^7946.0.16", "@types/geojson-vt": "3.2.5", diff --git a/src/source/mlt_vector_tile.ts b/src/source/mlt_vector_tile.ts new file mode 100644 index 00000000000..4014c4cec7a --- /dev/null +++ b/src/source/mlt_vector_tile.ts @@ -0,0 +1,86 @@ +import Point from '@mapbox/point-geometry'; +import {type VectorTile, type VectorTileFeature, type VectorTileLayer} from '@mapbox/vector-tile'; +import {type FeatureTable, decodeTile, type Feature as MLTFeature, GEOMETRY_TYPE} from '@maplibre/mlt'; + +type PublicPart = {[K in keyof T]: T[K]}; + +class MLTVectorTileFeature implements PublicPart { + _featureData: MLTFeature; + properties: {[_: string]: any}; + type: VectorTileFeature['type']; + extent: VectorTileFeature['extent']; + id: VectorTileFeature['id']; + + constructor(feature: MLTFeature, extent: number) { + this._featureData = feature; + this.properties = this._featureData.properties || {}; + switch (this._featureData.geometry?.type) { + case GEOMETRY_TYPE.POINT: + case GEOMETRY_TYPE.MULTIPOINT: + this.type = 1; + break; + case GEOMETRY_TYPE.LINESTRING: + case GEOMETRY_TYPE.MULTILINESTRING: + this.type = 2; + break; + case GEOMETRY_TYPE.POLYGON: + case GEOMETRY_TYPE.MULTIPOLYGON: + this.type = 3; + break; + default: + this.type = 0; + }; + this.extent = extent; + this.id = Number(this._featureData.id); + } + + toGeoJSON(_x: number, _y: number, _z: number): GeoJSON.Feature { + throw new Error('MLTVectorTileFeature.toGeoJSON not implemented'); + } + + loadGeometry(): Point[][] { + const points: Point[][] = []; + for (const ring of this._featureData.geometry.coordinates) { + const pointRing: Point[] = []; + for (const coord of ring) { + pointRing.push(new Point(coord.x, coord.y)); + } + points.push(pointRing); + } + return points; + } + bbox(): number[] { + return [0, 0, 0, 0]; + } +} + +class MLTVectorTileLayer implements PublicPart { + featureTable: FeatureTable; + name: string; + length: number; + version: number; + extent: number; + features: MLTFeature[] = []; + + constructor(featureTable: FeatureTable) { + this.featureTable = featureTable; + this.name = featureTable.name; + this.extent = featureTable.extent; + this.version = 2; + this.features = featureTable.getFeatures(); + this.length = this.features.length; + } + + feature(i: number): VectorTileFeature { + return new MLTVectorTileFeature(this.features[i], this.extent) as unknown as VectorTileFeature; + } +} + +export class MLTVectorTile implements VectorTile { + layers: Record = {}; + + constructor(buffer: ArrayBuffer) { + const features = decodeTile(new Uint8Array(buffer)); + this.layers = features.reduce((acc, f) => ({...acc, [f.name]: new MLTVectorTileLayer(f)}), {}); + } +} \ No newline at end of file diff --git a/src/source/vector_tile_source.ts b/src/source/vector_tile_source.ts index 39d542522ea..4d48555344e 100644 --- a/src/source/vector_tile_source.ts +++ b/src/source/vector_tile_source.ts @@ -20,7 +20,7 @@ export type VectorTileSourceOptions = VectorSourceSpecification & { }; /** - * A source containing vector tiles in [Mapbox Vector Tile format](https://docs.mapbox.com/vector-tiles/reference/). + * A source containing vector tiles in [Maplibre Vector Tile format](https://maplibre.org/maplibre-tile-spec/) or [Mapbox Vector Tile format](https://docs.mapbox.com/vector-tiles/reference/). * (See the [Style Specification](https://maplibre.org/maplibre-style-spec/) for detailed documentation of options.) * * @group Sources @@ -61,6 +61,7 @@ export class VectorTileSource extends Evented implements Source { maxzoom: number; url: string; scheme: string; + encoding: string; tileSize: number; promoteId: PromoteIdSpecification; @@ -90,7 +91,7 @@ export class VectorTileSource extends Evented implements Source { this.isTileClipped = true; this._loaded = false; - extend(this, pick(options, ['url', 'scheme', 'tileSize', 'promoteId'])); + extend(this, pick(options, ['url', 'scheme', 'tileSize', 'promoteId', 'encoding'])); this._options = extend({type: 'vector'}, options); this._collectResourceTiming = options.collectResourceTiming; @@ -202,7 +203,8 @@ export class VectorTileSource extends Evented implements Source { pixelRatio: this.map.getPixelRatio(), showCollisionBoxes: this.map.showCollisionBoxes, promoteId: this.promoteId, - subdivisionGranularity: this.map.style.projection.subdivisionGranularity + subdivisionGranularity: this.map.style.projection.subdivisionGranularity, + encoding: this.encoding }; params.request.collectResourceTiming = this._collectResourceTiming; let messageType: MessageType.loadTile | MessageType.reloadTile = MessageType.reloadTile; diff --git a/src/source/vector_tile_worker_source.ts b/src/source/vector_tile_worker_source.ts index ded45a64def..7d0a448ee07 100644 --- a/src/source/vector_tile_worker_source.ts +++ b/src/source/vector_tile_worker_source.ts @@ -15,6 +15,7 @@ import type { import type {IActor} from '../util/actor'; import type {StyleLayerIndex} from '../style/style_layer_index'; import {VectorTile} from '@mapbox/vector-tile'; +import {MLTVectorTile} from './mlt_vector_tile'; export type LoadVectorTileResult = { vectorTile: VectorTile; @@ -66,7 +67,9 @@ export class VectorTileWorkerSource implements WorkerSource { async loadVectorTile(params: WorkerTileParameters, abortController: AbortController): Promise { const response = await getArrayBuffer(params.request, abortController); try { - const vectorTile = new VectorTile(new Protobuf(response.data)); + const vectorTile = params.encoding !== 'mlt' + ? new VectorTile(new Protobuf(response.data)) + : new MLTVectorTile(response.data); return { vectorTile, rawData: response.data, diff --git a/src/source/worker_source.ts b/src/source/worker_source.ts index 704a7e818ef..3aadb3fb7f7 100644 --- a/src/source/worker_source.ts +++ b/src/source/worker_source.ts @@ -40,6 +40,7 @@ export type WorkerTileParameters = TileParameters & { collectResourceTiming?: boolean; returnDependencies?: boolean; subdivisionGranularity: SubdivisionGranularitySetting; + encoding?: string; }; /** diff --git a/test/build/min.test.ts b/test/build/min.test.ts index 4d2bb84e380..630487f0f9e 100644 --- a/test/build/min.test.ts +++ b/test/build/min.test.ts @@ -38,7 +38,7 @@ describe('test min build', () => { const decreaseQuota = 4096; // feel free to update this value after you've checked that it has changed on purpose :-) - const expectedBytes = 956986; + const expectedBytes = 1024126; expect(actualBytes).toBeLessThan(expectedBytes + increaseQuota); expect(actualBytes).toBeGreaterThan(expectedBytes - decreaseQuota); diff --git a/test/integration/assets/glyphs/Noto Sans Regular/256-511.pbf b/test/integration/assets/glyphs/Noto Sans Regular/256-511.pbf new file mode 100644 index 00000000000..6ae5c18a7d0 Binary files /dev/null and b/test/integration/assets/glyphs/Noto Sans Regular/256-511.pbf differ diff --git a/test/integration/assets/glyphs/Noto Sans Regular/7680-7935.pbf b/test/integration/assets/glyphs/Noto Sans Regular/7680-7935.pbf new file mode 100644 index 00000000000..5747c50e564 Binary files /dev/null and b/test/integration/assets/glyphs/Noto Sans Regular/7680-7935.pbf differ diff --git a/test/integration/assets/tiles/mlt/14/8716/5685.mlt b/test/integration/assets/tiles/mlt/14/8716/5685.mlt new file mode 100644 index 00000000000..42a03c63251 Binary files /dev/null and b/test/integration/assets/tiles/mlt/14/8716/5685.mlt differ diff --git a/test/integration/assets/tiles/mlt/14/8717/5679.mlt b/test/integration/assets/tiles/mlt/14/8717/5679.mlt new file mode 100644 index 00000000000..b7555462f62 Binary files /dev/null and b/test/integration/assets/tiles/mlt/14/8717/5679.mlt differ diff --git a/test/integration/assets/tiles/mlt/5/17/10.mlt b/test/integration/assets/tiles/mlt/5/17/10.mlt new file mode 100644 index 00000000000..52f0c41ec54 Binary files /dev/null and b/test/integration/assets/tiles/mlt/5/17/10.mlt differ diff --git a/test/integration/assets/tiles/mlt/5/22/12.mlt b/test/integration/assets/tiles/mlt/5/22/12.mlt new file mode 100644 index 00000000000..d556f1ee489 Binary files /dev/null and b/test/integration/assets/tiles/mlt/5/22/12.mlt differ diff --git a/test/integration/render/tests/mlt/render-layers/render-building/expected.png b/test/integration/render/tests/mlt/render-layers/render-building/expected.png new file mode 100644 index 00000000000..322ef2cc43f Binary files /dev/null and b/test/integration/render/tests/mlt/render-layers/render-building/expected.png differ diff --git a/test/integration/render/tests/mlt/render-layers/render-building/style.json b/test/integration/render/tests/mlt/render-layers/render-building/style.json new file mode 100644 index 00000000000..fb2667d8081 --- /dev/null +++ b/test/integration/render/tests/mlt/render-layers/render-building/style.json @@ -0,0 +1,33 @@ +{ + "version": 8, + "metadata": { + "test": { + "debug": true, + "height": 256 + } + }, + "center": [ + 11.525101, + 48.144716 + ], + "zoom": 14.92, + "sources": { + "openmaptiles": { + "type": "vector", + "encoding": "mlt", + "tiles": ["local://tiles/mlt/{z}/{x}/{y}.mlt"] + } + }, + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "building", + "type": "fill", + "source": "openmaptiles", + "source-layer": "building", + "paint": { + "fill-color": "#c86432" + } + } + ] +} diff --git a/test/integration/render/tests/mlt/render-layers/render-housenr/expected.png b/test/integration/render/tests/mlt/render-layers/render-housenr/expected.png new file mode 100644 index 00000000000..d8b21e60390 Binary files /dev/null and b/test/integration/render/tests/mlt/render-layers/render-housenr/expected.png differ diff --git a/test/integration/render/tests/mlt/render-layers/render-housenr/style.json b/test/integration/render/tests/mlt/render-layers/render-housenr/style.json new file mode 100644 index 00000000000..83f9cd047ba --- /dev/null +++ b/test/integration/render/tests/mlt/render-layers/render-housenr/style.json @@ -0,0 +1,40 @@ +{ + "version": 8, + "metadata": { + "test": { + "debug": true, + "height": 256 + } + }, + "center": [ + 11.529107, + 48.146443 + ], + "zoom": 17.81, + "sources": { + "openmaptiles": { + "type": "vector", + "encoding": "mlt", + "tiles": ["local://tiles/mlt/{z}/{x}/{y}.mlt"], + "maxzoom": 14 + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "housenumber", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "housenumber", + "layout": { + "text-field": "{housenumber}", + "text-font": ["Noto Sans Regular"], + "text-size": 30 + }, + "paint": { + "text-color": "#ffffff" + } + } + ] +} diff --git a/test/integration/render/tests/mlt/render-layers/render-labels/expected.png b/test/integration/render/tests/mlt/render-layers/render-labels/expected.png new file mode 100644 index 00000000000..e7f93cef930 Binary files /dev/null and b/test/integration/render/tests/mlt/render-layers/render-labels/expected.png differ diff --git a/test/integration/render/tests/mlt/render-layers/render-labels/style.json b/test/integration/render/tests/mlt/render-layers/render-labels/style.json new file mode 100644 index 00000000000..81fbf883b9c --- /dev/null +++ b/test/integration/render/tests/mlt/render-layers/render-labels/style.json @@ -0,0 +1,40 @@ +{ + "version": 8, + "metadata": { + "test": { + "debug": true, + "height": 256 + } + }, + "center": [ + 15.311276, + 50.821840 + ], + "zoom": 5.7, + "sources": { + "openmaptiles": { + "type": "vector", + "encoding": "mlt", + "tiles": ["local://tiles/mlt/{z}/{x}/{y}.mlt"], + "maxzoom": 14 + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "place_label_other", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "layout": { + "text-field": "{name:latin}", + "text-font": ["Noto Sans Regular"], + "text-size": ["interpolate", ["linear"], ["zoom"], 3, 12, 8, 22] + }, + "paint": { + "text-color": "#ffffff" + } + } + ] +} diff --git a/test/integration/render/tests/mlt/render-layers/render-landcover/expected.png b/test/integration/render/tests/mlt/render-layers/render-landcover/expected.png new file mode 100644 index 00000000000..750552ee676 Binary files /dev/null and b/test/integration/render/tests/mlt/render-layers/render-landcover/expected.png differ diff --git a/test/integration/render/tests/mlt/render-layers/render-landcover/style.json b/test/integration/render/tests/mlt/render-layers/render-landcover/style.json new file mode 100644 index 00000000000..1de5b9482af --- /dev/null +++ b/test/integration/render/tests/mlt/render-layers/render-landcover/style.json @@ -0,0 +1,34 @@ +{ + "version": 8, + "metadata": { + "test": { + "debug": true, + "height": 256 + } + }, + "center": [ + 73, + 36 + ], + "zoom": 5.99, + "sources": { + "openmaptiles": { + "type": "vector", + "encoding": "mlt", + "tiles": ["local://tiles/mlt/{z}/{x}/{y}.mlt"], + "maxzoom": 14 + } + }, + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "landcover", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "paint": { + "fill-color": "#008000" + } + } + ] +} diff --git a/test/integration/render/tests/mlt/render-layers/render-roads/expected.png b/test/integration/render/tests/mlt/render-layers/render-roads/expected.png new file mode 100644 index 00000000000..d5432900adf Binary files /dev/null and b/test/integration/render/tests/mlt/render-layers/render-roads/expected.png differ diff --git a/test/integration/render/tests/mlt/render-layers/render-roads/style.json b/test/integration/render/tests/mlt/render-layers/render-roads/style.json new file mode 100644 index 00000000000..6ef3b8e3498 --- /dev/null +++ b/test/integration/render/tests/mlt/render-layers/render-roads/style.json @@ -0,0 +1,34 @@ +{ + "version": 8, + "metadata": { + "test": { + "debug": true, + "height": 256 + } + }, + "center": [ + 11.534169, + 48.145262 + ], + "zoom": 15.80, + "sources": { + "openmaptiles": { + "type": "vector", + "encoding": "mlt", + "tiles": ["local://tiles/mlt/{z}/{x}/{y}.mlt"], + "maxzoom": 14 + } + }, + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "road", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "paint": { + "line-color": "#00FF00" + } + } + ] +} diff --git a/test/integration/render/tests/mlt/render-layers/render-water/expected.png b/test/integration/render/tests/mlt/render-layers/render-water/expected.png new file mode 100644 index 00000000000..c737198f0f6 Binary files /dev/null and b/test/integration/render/tests/mlt/render-layers/render-water/expected.png differ diff --git a/test/integration/render/tests/mlt/render-layers/render-water/style.json b/test/integration/render/tests/mlt/render-layers/render-water/style.json new file mode 100644 index 00000000000..a55e1155c00 --- /dev/null +++ b/test/integration/render/tests/mlt/render-layers/render-water/style.json @@ -0,0 +1,46 @@ +{ + "version": 8, + "metadata": { + "test": { + "debug": true, + "height": 256 + } + }, + "center": [ + 14.495088, + 53.755635 + ], + "zoom": 5.74, + "sources": { + "openmaptiles": { + "type": "vector", + "encoding": "mlt", + "tiles": ["local://tiles/mlt/{z}/{x}/{y}.mlt"], + "maxzoom": 14 + } + }, + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "water", + "type": "fill", + "source": "openmaptiles", + "source-layer": "water", + "filter": ["!=", "intermittent", 1], + "paint": { + "fill-color": "#F08000" + } + }, + { + "id": "waterway", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": ["!=", "intermittent", 1], + "paint": { + "line-color": "#008000" + } + } + ] + +}