diff --git a/CHANGELOG.md b/CHANGELOG.md index 28452f46b2..794d9ca975 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### ✨ Features and improvements - Add Etag unmodified support to optimize vector tile reloading ([#7074](https://github.com/maplibre/maplibre-gl-js/pull/7074)) (by [@rivkamatan](https://github.com/rivkamatan and [@wayofthefuture](https://github.com/wayofthefuture)) +- ⚠️ Support geojson nested objects ([#6992](https://github.com/maplibre/maplibre-gl-js/pull/6992)) (by [HarelM](https://github.com/HarelM)) + - _...Add new stuff here..._ ### 🐞 Bug fixes diff --git a/src/data/feature_index.ts b/src/data/feature_index.ts index 25669d0eeb..ced9f3af36 100644 --- a/src/data/feature_index.ts +++ b/src/data/feature_index.ts @@ -1,4 +1,5 @@ import type Point from '@mapbox/point-geometry'; +import {type VectorTileFeatureLike, type VectorTileLayerLike, GEOJSON_TILE_LAYER_NAME} from '@maplibre/vt-pbf'; import {loadGeometry} from './load_geometry'; import {toEvaluationFeature} from './evaluation_feature'; import {EXTENT} from './extent'; @@ -13,9 +14,10 @@ import {EvaluationParameters} from '../style/evaluation_parameters'; import {polygonIntersectsBox} from '../util/intersection_tests'; import {PossiblyEvaluated} from '../style/properties'; import {FeatureIndexArray} from './array_types.g'; - import {MLTVectorTile} from '../source/vector_tile_mlt'; import {Bounds} from '../geo/bounds'; +import {VectorTile} from '@mapbox/vector-tile'; + import type {OverscaledTileID} from '../tile/tile_id'; import type {SourceFeatureState} from '../source/source_state'; import type {mat4} from 'gl-matrix'; @@ -23,8 +25,7 @@ import type {MapGeoJSONFeature} from '../util/vectortile_to_geojson'; import type {StyleLayer} from '../style/style_layer'; import type {FeatureFilter, FeatureState, FilterSpecification, PromoteIdSpecification} from '@maplibre/maplibre-gl-style-spec'; import type {IReadonlyTransform} from '../geo/transform_interface'; -import {type VectorTileFeatureLike, type VectorTileLayerLike, GEOJSON_TILE_LAYER_NAME} from '@maplibre/vt-pbf'; -import {VectorTile} from '@mapbox/vector-tile'; +import type {TileEncoding} from '../source/worker_source'; export {GEOJSON_TILE_LAYER_NAME}; @@ -67,7 +68,7 @@ export class FeatureIndex { grid3D: TransferableGridIndex; featureIndexArray: FeatureIndexArray; promoteId?: PromoteIdSpecification; - encoding: string; + encoding: TileEncoding; rawTileData: ArrayBuffer; bucketLayerIDs: Array>; @@ -114,9 +115,14 @@ export class FeatureIndex { loadVTLayers(): {[_: string]: VectorTileLayerLike} { if (!this.vtLayers) { - this.vtLayers = this.encoding !== 'mlt' - ? new VectorTile(new Protobuf(this.rawTileData)).layers - : new MLTVectorTile(this.rawTileData).layers; + switch (this.encoding) { + case 'mlt': + this.vtLayers = new MLTVectorTile(this.rawTileData).layers; + break; + case 'mvt': + default: + this.vtLayers = new VectorTile(new Protobuf(this.rawTileData)).layers; + } this.sourceLayerCoder = new DictionaryCoder(this.vtLayers ? Object.keys(this.vtLayers).sort() : [GEOJSON_TILE_LAYER_NAME]); } return this.vtLayers; diff --git a/src/source/geojson_worker_source.test.ts b/src/source/geojson_worker_source.test.ts index 56e4b6b488..c116a4f524 100644 --- a/src/source/geojson_worker_source.test.ts +++ b/src/source/geojson_worker_source.test.ts @@ -218,6 +218,7 @@ describe('reloadTile', () => { let data = await source.reloadTile(tileParams as any as WorkerTileParameters) as WorkerTileWithData; expect('rawTileData' in data).toBeFalsy(); data.rawTileData = firstData.rawTileData; + data.encoding = 'mvt'; expect(data).toEqual(firstData); // also shouldn't call loadVectorData again diff --git a/src/source/geojson_worker_source.ts b/src/source/geojson_worker_source.ts index f26f62388d..86ce4841bf 100644 --- a/src/source/geojson_worker_source.ts +++ b/src/source/geojson_worker_source.ts @@ -1,17 +1,16 @@ import {getJSON} from '../util/ajax'; import {RequestPerformance} from '../util/performance'; import rewind from '@mapbox/geojson-rewind'; -import {GeoJSONWrapper} from '@maplibre/vt-pbf'; +import {fromVectorTileJs, GeoJSONWrapper} from '@maplibre/vt-pbf'; import {EXTENT} from '../data/extent'; import Supercluster, {type Options as SuperclusterOptions, type ClusterProperties} from 'supercluster'; import geojsonvt, {type GeoJSONVTOptions, type GeoJSONVT} from '@maplibre/geojson-vt'; import {createExpression} from '@maplibre/maplibre-gl-style-spec'; import {isAbortError} from '../util/abort_error'; -import {toVirtualVectorTile} from './vector_tile_overzoomed'; import {type GeoJSONSourceDiff, applySourceDiff, toUpdateable, type GeoJSONFeatureId} from './geojson_source_diff'; import {WorkerTile} from './worker_tile'; import {WorkerTileState, type ParsingState} from './worker_tile_state'; -import {extend} from '../util/util'; +import {extend, JSON_PREFIX} from '../util/util'; import type {WorkerSource, WorkerTileParameters, TileParameters, WorkerTileResult} from './worker_source'; import type {LoadVectorTileResult} from './vector_tile_worker_source'; @@ -99,7 +98,11 @@ export class GeoJSONWorkerSource implements WorkerSource { if (!geoJSONTile) return null; const geojsonWrapper = new GeoJSONWrapper(geoJSONTile.features, {version: 2, extent: EXTENT}); - return toVirtualVectorTile(geojsonWrapper); + return { + vectorTile: geojsonWrapper, + rawData: fromVectorTileJs(geojsonWrapper, JSON_PREFIX).buffer + }; + } /** @@ -159,7 +162,7 @@ export class GeoJSONWorkerSource implements WorkerSource { if (parseState) { const {rawData} = parseState; // Transferring a copy of rawTileData because the worker needs to retain its copy. - result = extend({rawTileData: rawData.slice(0)}, result); + result = extend({rawTileData: rawData.slice(0), encoding: 'mvt'}, result); } return result; diff --git a/src/source/vector_tile_overzoomed.ts b/src/source/vector_tile_overzoomed.ts index 542d8dcb75..30538e33b3 100644 --- a/src/source/vector_tile_overzoomed.ts +++ b/src/source/vector_tile_overzoomed.ts @@ -1,8 +1,7 @@ import Point from '@mapbox/point-geometry'; -import {type VectorTileFeatureLike, type VectorTileLayerLike, type VectorTileLike, fromVectorTileJs} from '@maplibre/vt-pbf'; import {clipGeometry} from '../symbol/clip_line'; -import type {LoadVectorTileResult} from './vector_tile_worker_source'; import type {CanonicalTileID} from '../tile/tile_id'; +import type {VectorTileFeatureLike, VectorTileLayerLike, VectorTileLike} from '@maplibre/vt-pbf'; class VectorTileFeatureOverzoomed implements VectorTileFeatureLike { pointsArray: Point[][]; @@ -60,23 +59,6 @@ export class VectorTileOverzoomed implements VectorTileLike { } } -/** - * Encodes the virtual tile into binary vector tile form. - * This is a convenience that allows `FeatureIndex` to operate the same way across `VectorTileSource` and `GeoJSONSource` data. - * @param virtualVectorTile - a syntetically created vector tile, this tile should have the relevant layer and features already added to it. - * @returns - the encoded vector tile along with the original virtual tile binary data. - */ -export function toVirtualVectorTile(virtualVectorTile: VectorTileLike): LoadVectorTileResult { - let pbf: Uint8Array = fromVectorTileJs(virtualVectorTile); - if (pbf.byteOffset !== 0 || pbf.byteLength !== pbf.buffer.byteLength) { - pbf = new Uint8Array(pbf); // Compatibility with node Buffer (https://github.com/mapbox/pbf/issues/35) - } - return { - vectorTile: virtualVectorTile, - rawData: pbf.buffer - }; -} - /** * This function slices a source tile layer into an overzoomed tile layer for a target tile ID. * @param sourceLayer - the source tile layer to slice diff --git a/src/source/vector_tile_source.ts b/src/source/vector_tile_source.ts index 14f9678a43..b7bee9bf14 100644 --- a/src/source/vector_tile_source.ts +++ b/src/source/vector_tile_source.ts @@ -13,7 +13,7 @@ import type {Map} from '../ui/map'; import type {Dispatcher} from '../util/dispatcher'; import type {Tile} from '../tile/tile'; import type {VectorSourceSpecification, PromoteIdSpecification} from '@maplibre/maplibre-gl-style-spec'; -import type {WorkerTileParameters, OverzoomParameters, WorkerTileResult} from './worker_source'; +import type {WorkerTileParameters, OverzoomParameters, WorkerTileResult, TileEncoding} from './worker_source'; export type VectorTileSourceOptions = VectorSourceSpecification & { collectResourceTiming?: boolean; @@ -69,7 +69,7 @@ export class VectorTileSource extends Evented implements Source { maxzoom: number; url: string; scheme: string; - encoding: string; + encoding: TileEncoding; tileSize: number; promoteId: PromoteIdSpecification; diff --git a/src/source/vector_tile_worker_source.test.ts b/src/source/vector_tile_worker_source.test.ts index 680368e00e..4ac48c571c 100644 --- a/src/source/vector_tile_worker_source.test.ts +++ b/src/source/vector_tile_worker_source.test.ts @@ -174,7 +174,8 @@ describe('vector tile worker source', () => { const loadVectorData = (_params, _rawData) => { return { vectorTile: new VectorTile(new Protobuf(rawTileData)), - rawData: rawTileData + rawData: rawTileData, + encoding: 'mvt' }; }; @@ -343,7 +344,8 @@ describe('vector tile worker source', () => { vectorTile: new VectorTile(new Protobuf(rawTileData)), rawData: rawTileData, cacheControl: null, - expires: null + expires: null, + encoding: 'mvt' }; }; @@ -406,7 +408,8 @@ describe('vector tile worker source', () => { vectorTile: new VectorTile(new Protobuf(rawTileData)), rawData: rawTileData, cacheControl: null, - expires: null + expires: null, + encoding: 'mvt' }; }; diff --git a/src/source/vector_tile_worker_source.ts b/src/source/vector_tile_worker_source.ts index 756c5d0b00..8742e8f5b6 100644 --- a/src/source/vector_tile_worker_source.ts +++ b/src/source/vector_tile_worker_source.ts @@ -1,12 +1,13 @@ import Protobuf from 'pbf'; import {VectorTile} from '@mapbox/vector-tile'; +import {fromVectorTileJs, type VectorTileLayerLike, type VectorTileLike} from '@maplibre/vt-pbf'; import {type ExpiryData, getArrayBuffer} from '../util/ajax'; import {WorkerTile} from './worker_tile'; import {WorkerTileState, type ParsingState} from './worker_tile_state'; import {BoundedLRUCache} from '../tile/tile_cache'; import {extend} from '../util/util'; import {RequestPerformance} from '../util/performance'; -import {VectorTileOverzoomed, sliceVectorTileLayer, toVirtualVectorTile} from './vector_tile_overzoomed'; +import {VectorTileOverzoomed, sliceVectorTileLayer} from './vector_tile_overzoomed'; import {MLTVectorTile} from './vector_tile_mlt'; import type { WorkerSource, @@ -17,7 +18,6 @@ import type { import type {IActor} from '../util/actor'; import type {StyleLayer} from '../style/style_layer'; import type {StyleLayerIndex} from '../style/style_layer_index'; -import type {VectorTileLayerLike, VectorTileLike} from '@maplibre/vt-pbf'; export type LoadVectorTileResult = { vectorTile: VectorTileLike; @@ -193,7 +193,10 @@ export class VectorTileWorkerSource implements WorkerSource { overzoomedVectorTile.addLayer(slicedTileLayer); } } - const overzoomedVectorTileResult = toVirtualVectorTile(overzoomedVectorTile); + const overzoomedVectorTileResult = { + vectorTile: overzoomedVectorTile, + rawData: fromVectorTileJs(overzoomedVectorTile).buffer + }; this.overzoomedTileResultCache.set(cacheKey, overzoomedVectorTileResult); return overzoomedVectorTileResult; diff --git a/src/source/worker_source.ts b/src/source/worker_source.ts index 9ca15b7f4f..ba20e170b1 100644 --- a/src/source/worker_source.ts +++ b/src/source/worker_source.ts @@ -16,6 +16,8 @@ import type {StyleLayerIndex} from '../style/style_layer_index'; import type {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings'; import type {DashEntry} from '../render/line_atlas'; +export type TileEncoding = 'mlt' | 'mvt'; + /** * Parameters to identify a tile */ @@ -40,7 +42,7 @@ export type WorkerTileParameters = TileParameters & { collectResourceTiming?: boolean; returnDependencies?: boolean; subdivisionGranularity: SubdivisionGranularitySetting; - encoding?: string; + encoding?: TileEncoding; /** * Provide this property when the requested tile has a higher canonical Z than source maxzoom. * This allows the worker to know that it needs to overzoom from a source tile. @@ -80,7 +82,7 @@ export type WorkerTileWithData = ExpiryData & { featureIndex: FeatureIndex; collisionBoxArray: CollisionBoxArray; rawTileData?: ArrayBuffer; - encoding?: string; + encoding?: TileEncoding; resourceTiming?: Array; // Only used for benchmarking: glyphMap?: { diff --git a/src/tile/tile.ts b/src/tile/tile.ts index 7eff9d93bf..212b5b2575 100644 --- a/src/tile/tile.ts +++ b/src/tile/tile.ts @@ -11,12 +11,10 @@ import {toEvaluationFeature} from '../data/evaluation_feature'; import {EvaluationParameters} from '../style/evaluation_parameters'; import {rtlMainThreadPluginFactory} from '../source/rtl_text_plugin_main_thread'; -const CLOCK_SKEW_RETRY_TIMEOUT = 30000; - import type {SourceFeatureState} from '../source/source_state'; import type {Bucket} from '../data/bucket'; import type {StyleLayer} from '../style/style_layer'; -import type {WorkerTileResult} from '../source/worker_source'; +import type {TileEncoding, WorkerTileResult} from '../source/worker_source'; import type {Actor} from '../util/actor'; import type {DEMData} from '../data/dem_data'; import type {AlphaImage} from '../util/image'; @@ -34,6 +32,9 @@ import type {QueryRenderedFeaturesOptionsStrict, QuerySourceFeatureOptionsStrict import type {DashEntry} from '../render/line_atlas'; import type {VectorTileLayerLike} from '@maplibre/vt-pbf'; import type {Painter} from '../render/painter'; + +const CLOCK_SKEW_RETRY_TIMEOUT = 30000; + /** * The tile's state, can be: * @@ -73,7 +74,7 @@ export class Tile { buckets: {[_: string]: Bucket}; latestFeatureIndex: FeatureIndex | null; latestRawTileData: ArrayBuffer; - latestEncoding: string; + latestEncoding: TileEncoding; imageAtlas: ImageAtlas; imageAtlasTexture: Texture; dashPositions: {[_: string]: DashEntry}; diff --git a/src/util/util.ts b/src/util/util.ts index 5afbe0eeaa..f6419f7f3f 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -8,6 +8,8 @@ import {pixelsToTileUnits} from '../source/pixels_to_tile_units'; import {type OverscaledTileID} from '../tile/tile_id'; import type {Event} from './evented'; +export const JSON_PREFIX = '__$json__:'; + /** * Returns a new 64 bit float vec4 of zeroes. */ diff --git a/src/util/vectortile_to_geojson.ts b/src/util/vectortile_to_geojson.ts index 9b7a7e2c64..caee80f9fd 100644 --- a/src/util/vectortile_to_geojson.ts +++ b/src/util/vectortile_to_geojson.ts @@ -1,5 +1,6 @@ import type Point from '@mapbox/point-geometry'; import {classifyRings} from '@mapbox/vector-tile'; +import {JSON_PREFIX} from './util'; import type {LayerSpecification} from '@maplibre/maplibre-gl-style-spec'; import type {VectorTileFeatureLike} from '@maplibre/vt-pbf'; @@ -45,6 +46,13 @@ export class GeoJSONFeature { this._y = y; this._z = z; + for (const key in vectorTileFeature.properties) { + if (typeof vectorTileFeature.properties[key] !== 'string' || !vectorTileFeature.properties[key].startsWith(JSON_PREFIX)) { + continue; + } + // JSON parsing the special case of a json prefix that is serialized in geojson worker source. + vectorTileFeature.properties[key] = JSON.parse(vectorTileFeature.properties[key].slice(JSON_PREFIX.length)); + } this.properties = vectorTileFeature.properties; this.id = id; } diff --git a/test/build/bundle_size.json b/test/build/bundle_size.json index 6fffd328c8..799cc94578 100644 --- a/test/build/bundle_size.json +++ b/test/build/bundle_size.json @@ -1 +1 @@ -1023690 +1024823 diff --git a/test/examples/measure-distances.html b/test/examples/measure-distances.html index 96f04d75b1..01f26a08ab 100644 --- a/test/examples/measure-distances.html +++ b/test/examples/measure-distances.html @@ -147,16 +147,16 @@ map.getSource('geojson').setData(geojson); }); - }); - map.on('mousemove', (e) => { - const features = map.queryRenderedFeatures(e.point, { - layers: ['measure-points'] + map.on('mousemove', (e) => { + const features = map.queryRenderedFeatures(e.point, { + layers: ['measure-points'] + }); + // UI indicator for clicking/hovering a point on the map + map.getCanvas().style.cursor = features.length ? + 'pointer' : + 'crosshair'; }); - // UI indicator for clicking/hovering a point on the map - map.getCanvas().style.cursor = features.length ? - 'pointer' : - 'crosshair'; }); diff --git a/test/integration/query/tests/properties/geojson-nested-properties/expected.json b/test/integration/query/tests/properties/geojson-nested-properties/expected.json new file mode 100644 index 0000000000..194324828c --- /dev/null +++ b/test/integration/query/tests/properties/geojson-nested-properties/expected.json @@ -0,0 +1,21 @@ +[ + { + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 0 + ] + }, + "type": "Feature", + "properties": { + "nested": { + "nested2": { + "num": 1 + } + } + }, + "source": "geojson", + "state": {} + } +] \ No newline at end of file diff --git a/test/integration/query/tests/properties/geojson-nested-properties/style.json b/test/integration/query/tests/properties/geojson-nested-properties/style.json new file mode 100644 index 0000000000..f0ebc0079d --- /dev/null +++ b/test/integration/query/tests/properties/geojson-nested-properties/style.json @@ -0,0 +1,51 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "debug": true, + "queryGeometry": [ + [ + 0, + 0 + ], + [ + 512, + 512 + ] + ] + } + }, + "center": [ + 0, + 0 + ], + "zoom": 0, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": { + "nested": { + "nested2": { + "num": 1 + } + } + }, + "geometry": { + "type": "Point", + "coordinates": [0, 0] + } + } + } + }, + "layers": [ + { + "id": "circles", + "type": "circle", + "source": "geojson" + } + ] +} \ No newline at end of file