From a11eff11d1fd8597eb6165071c0ec88a8e58301a Mon Sep 17 00:00:00 2001 From: HarelM Date: Sun, 29 Dec 2024 09:23:11 +0200 Subject: [PATCH 1/6] Initial basic refactoring --- src/source/query_features.ts | 7 ++----- src/style/style_layer/circle_style_layer.ts | 6 +++--- src/style/style_layer/fill_extrusion_style_layer.ts | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/source/query_features.ts b/src/source/query_features.ts index ab31bb35aa..750ede05d2 100644 --- a/src/source/query_features.ts +++ b/src/source/query_features.ts @@ -62,11 +62,8 @@ function getPixelPosMatrix(transform, tileID) { const t = mat4.create(); mat4.translate(t, t, [1, 1, 0]); mat4.scale(t, t, [transform.width * 0.5, transform.height * 0.5, 1]); - if (transform.calculatePosMatrix) { // Globe: TODO: remove this hack once queryRendererFeatures supports globe properly - return mat4.multiply(t, t, transform.calculatePosMatrix(tileID.toUnwrapped())); - } else { - return t; - } + const projectionData = transform.getProjectionData({overscaledTileID: tileID.toUnwrapped()}); + return mat4.multiply(t, t, projectionData.mainMatrix); } function queryIncludes3DLayer(layers: Set | undefined, styleLayers: {[_: string]: StyleLayer}, sourceID: string) { diff --git a/src/style/style_layer/circle_style_layer.ts b/src/style/style_layer/circle_style_layer.ts index b4a9c64e54..1be3dfcc98 100644 --- a/src/style/style_layer/circle_style_layer.ts +++ b/src/style/style_layer/circle_style_layer.ts @@ -60,9 +60,9 @@ export class CircleStyleLayer extends StyleLayer { const size = radius + stroke; // For pitch-alignment: map, compare feature geometry to query geometry in the plane of the tile - // // Otherwise, compare geometry in the plane of the viewport - // // A circle with fixed scaling relative to the viewport gets larger in tile space as it moves into the distance - // // A circle with fixed scaling relative to the map gets smaller in viewport space as it moves into the distance + // Otherwise, compare geometry in the plane of the viewport + // A circle with fixed scaling relative to the viewport gets larger in tile space as it moves into the distance + // A circle with fixed scaling relative to the map gets smaller in viewport space as it moves into the distance const alignWithMap = this.paint.get('circle-pitch-alignment') === 'map'; const transformedPolygon = alignWithMap ? translatedPolygon : projectQueryGeometry(translatedPolygon, pixelPosMatrix); const transformedSize = alignWithMap ? size * pixelsToTileUnits : size; diff --git a/src/style/style_layer/fill_extrusion_style_layer.ts b/src/style/style_layer/fill_extrusion_style_layer.ts index 9b1240b931..1ce1ab5d44 100644 --- a/src/style/style_layer/fill_extrusion_style_layer.ts +++ b/src/style/style_layer/fill_extrusion_style_layer.ts @@ -59,7 +59,7 @@ export class FillExtrusionStyleLayer extends StyleLayer { const height = this.paint.get('fill-extrusion-height').evaluate(feature, featureState); const base = this.paint.get('fill-extrusion-base').evaluate(feature, featureState); - const projectedQueryGeometry = projectQueryGeometry(translatedPolygon, pixelPosMatrix, transform, 0); + const projectedQueryGeometry = projectQueryGeometry(translatedPolygon, pixelPosMatrix, 0); const projected = projectExtrusion(geometry, base, height, pixelPosMatrix); const projectedBase = projected[0]; @@ -215,7 +215,7 @@ function projectExtrusion(geometry: Array>, zBase: number, zTop: nu return [projectedBase, projectedTop]; } -function projectQueryGeometry(queryGeometry: Array, pixelPosMatrix: mat4, transform: IReadonlyTransform, z: number) { +function projectQueryGeometry(queryGeometry: Array, pixelPosMatrix: mat4, z: number) { const projectedQueryGeometry = []; for (const p of queryGeometry) { const v = [p.x, p.y, z, 1] as vec4; From 3a6473fa916553ad60ceed3480e20078b20b7f3e Mon Sep 17 00:00:00 2001 From: HarelM Date: Mon, 30 Dec 2024 13:40:13 +0200 Subject: [PATCH 2/6] Added more types to better understand what's going on... --- src/data/feature_index.ts | 37 ++++--- src/source/query_features.ts | 101 +++++++++++------- src/source/source_cache.ts | 2 +- src/source/tile.ts | 11 +- src/style/query_utils.ts | 14 ++- src/style/style.test.ts | 27 ++--- src/style/style.ts | 15 +-- src/style/style_layer.ts | 49 +++++++-- src/style/style_layer/circle_style_layer.ts | 23 ++-- .../style_layer/fill_extrusion_style_layer.ts | 23 ++-- src/style/style_layer/fill_style_layer.ts | 23 ++-- src/style/style_layer/line_style_layer.ts | 23 ++-- src/ui/map.ts | 6 +- 13 files changed, 204 insertions(+), 150 deletions(-) diff --git a/src/data/feature_index.ts b/src/data/feature_index.ts index 9908708112..41a761d0b6 100644 --- a/src/data/feature_index.ts +++ b/src/data/feature_index.ts @@ -39,6 +39,16 @@ type QueryParameters = { }; }; +export type QueryResults = { + [_: string]: QueryResultsItem[]; +} + +export type QueryResultsItem = { + featureIndex: number; + feature: GeoJSONFeature; + intersectionZ?: boolean | number; +} + /** * An in memory index class to allow fast interaction with features */ @@ -110,7 +120,7 @@ export class FeatureIndex { styleLayers: {[_: string]: StyleLayer}, serializedLayers: {[_: string]: any}, sourceFeatureState: SourceFeatureState - ): {[_: string]: Array<{featureIndex: number; feature: GeoJSONFeature}>} { + ): QueryResults { this.loadVTLayers(); const params = args.params; @@ -136,7 +146,7 @@ export class FeatureIndex { matching.sort(topDownFeatureComparator); - const result = {}; + const result: QueryResults = {}; let previousIndex; for (let k = 0; k < matching.length; k++) { const index = matching[k]; @@ -163,7 +173,16 @@ export class FeatureIndex { featureGeometry = loadGeometry(feature); } - return styleLayer.queryIntersectsFeature(queryGeometry, feature, featureState, featureGeometry, this.z, args.transform, pixelsToTileUnits, args.pixelPosMatrix); + return styleLayer.queryIntersectsFeature({ + queryGeometry, + feature, + featureState, + geometry: featureGeometry, + zoom: this.z, + transform: args.transform, + pixelsToTileUnits, + pixelPosMatrix: args.pixelPosMatrix + }); } ); } @@ -172,13 +191,7 @@ export class FeatureIndex { } loadMatchingFeature( - result: { - [_: string]: Array<{ - featureIndex: number; - feature: GeoJSONFeature; - intersectionZ?: boolean | number; - }>; - }, + result: QueryResults, bucketIndex: number, sourceLayerIndex: number, featureIndex: number, @@ -261,8 +274,8 @@ export class FeatureIndex { filterSpec: FilterSpecification, filterLayerIDs: Set | null, availableImages: Array, - styleLayers: {[_: string]: StyleLayer}) { - const result = {}; + styleLayers: {[_: string]: StyleLayer}): QueryResults { + const result: QueryResults = {}; this.loadVTLayers(); const filter = featureFilter(filterSpec); diff --git a/src/source/query_features.ts b/src/source/query_features.ts index 750ede05d2..f07c902bfa 100644 --- a/src/source/query_features.ts +++ b/src/source/query_features.ts @@ -1,12 +1,19 @@ +import {mat4} from 'gl-matrix'; +import type Point from '@mapbox/point-geometry'; import type {SourceCache} from './source_cache'; import type {StyleLayer} from '../style/style_layer'; import type {CollisionIndex} from '../symbol/collision_index'; import type {IReadonlyTransform} from '../geo/transform_interface'; import type {RetainedQueryData} from '../symbol/placement'; import type {FilterSpecification} from '@maplibre/maplibre-gl-style-spec'; -import type {MapGeoJSONFeature} from '../util/vectortile_to_geojson'; -import type Point from '@mapbox/point-geometry'; -import {mat4} from 'gl-matrix'; +import type {GeoJSONFeature, MapGeoJSONFeature} from '../util/vectortile_to_geojson'; +import type {QueryResults, QueryResultsItem} from '../data/feature_index'; +import type {OverscaledTileID} from './tile_id'; + +type RenderedFeatureLayer = { + wrappedTileID: string; + queryResults: QueryResults; +} /** * Options to pass to query the map for the rendered features @@ -55,10 +62,16 @@ export type QuerySourceFeatureOptions = { validate?: boolean; } +export type QueryRenderedFeaturesResults = { + [key: string]: QueryRenderedFeaturesResultsItem[]; +} + +export type QueryRenderedFeaturesResultsItem = QueryResultsItem & { feature: MapGeoJSONFeature }; + /* * Returns a matrix that can be used to convert from tile coordinates to viewport pixel coordinates. */ -function getPixelPosMatrix(transform, tileID) { +function getPixelPosMatrix(transform: IReadonlyTransform, tileID) { const t = mat4.create(); mat4.translate(t, t, [1, 1, 0]); mat4.scale(t, t, [transform.width * 0.5, transform.height * 0.5, 1]); @@ -92,14 +105,14 @@ export function queryRenderedFeatures( queryGeometry: Array, params: QueryRenderedFeaturesOptionsStrict | undefined, transform: IReadonlyTransform -): { [key: string]: Array<{featureIndex: number; feature: MapGeoJSONFeature}> } { +): QueryRenderedFeaturesResults { const has3DLayer = queryIncludes3DLayer(params?.layers ?? null, styleLayers, sourceCache.id); const maxPitchScaleFactor = transform.maxPitchScaleFactor(); const tilesIn = sourceCache.tilesIn(queryGeometry, maxPitchScaleFactor, has3DLayer); tilesIn.sort(sortTilesIn); - const renderedFeatureLayers = []; + const renderedFeatureLayers: RenderedFeatureLayer[] = []; for (const tileIn of tilesIn) { renderedFeatureLayers.push({ wrappedTileID: tileIn.tileID.wrapped().key, @@ -119,19 +132,7 @@ export function queryRenderedFeatures( const result = mergeRenderedFeatureLayers(renderedFeatureLayers); - // Merge state from SourceCache into the results - for (const layerID in result) { - result[layerID].forEach((featureWrapper) => { - const feature = featureWrapper.feature as MapGeoJSONFeature; - const state = sourceCache.getFeatureState(feature.layer['source-layer'], feature.id); - feature.source = feature.layer.source; - if (feature.layer['source-layer']) { - feature.sourceLayer = feature.layer['source-layer']; - } - feature.state = state; - }); - } - return result; + return convertFeaturesToMapFeatures(result, sourceCache); } export function queryRenderedSymbols(styleLayers: {[_: string]: StyleLayer}, @@ -142,8 +143,8 @@ export function queryRenderedSymbols(styleLayers: {[_: string]: StyleLayer}, collisionIndex: CollisionIndex, retainedQueryData: { [_: number]: RetainedQueryData; - }) { - const result = {}; + }): QueryRenderedFeaturesResults { + const result: QueryResults = {}; const renderedSymbols = collisionIndex.queryRenderedSymbols(queryGeometry); const bucketQueryData: RetainedQueryData[] = []; for (const bucketInstanceId of Object.keys(renderedSymbols).map(Number)) { @@ -189,29 +190,15 @@ export function queryRenderedSymbols(styleLayers: {[_: string]: StyleLayer}, } } - // Merge state from SourceCache into the results - for (const layerName in result) { - result[layerName].forEach((featureWrapper) => { - const feature = featureWrapper.feature; - const layer = styleLayers[layerName]; - const sourceCache = sourceCaches[layer.source]; - const state = sourceCache.getFeatureState(feature.layer['source-layer'], feature.id); - feature.source = feature.layer.source; - if (feature.layer['source-layer']) { - feature.sourceLayer = feature.layer['source-layer']; - } - feature.state = state; - }); - } - return result; + return convertFeaturesToMapFeaturesMultiple(result, styleLayers, sourceCaches); } -export function querySourceFeatures(sourceCache: SourceCache, params: QuerySourceFeatureOptions | undefined) { +export function querySourceFeatures(sourceCache: SourceCache, params: QuerySourceFeatureOptions | undefined): GeoJSONFeature[] { const tiles = sourceCache.getRenderableIds().map((id) => { return sourceCache.getTileByID(id); }); - const result = []; + const result: GeoJSONFeature[] = []; const dataTiles = {}; for (let i = 0; i < tiles.length; i++) { @@ -226,16 +213,16 @@ export function querySourceFeatures(sourceCache: SourceCache, params: QuerySourc return result; } -function sortTilesIn(a, b) { +function sortTilesIn(a: {tileID: OverscaledTileID}, b: {tileID: OverscaledTileID}) { const idA = a.tileID; const idB = b.tileID; return (idA.overscaledZ - idB.overscaledZ) || (idA.canonical.y - idB.canonical.y) || (idA.wrap - idB.wrap) || (idA.canonical.x - idB.canonical.x); } -function mergeRenderedFeatureLayers(tiles) { +function mergeRenderedFeatureLayers(tiles: RenderedFeatureLayer[]): QueryResults { // Merge results from all tiles, but if two tiles share the same // wrapped ID, don't duplicate features between the two tiles - const result = {}; + const result: QueryResults = {}; const wrappedIDLayerMap = {}; for (const tile of tiles) { const queryResults = tile.queryResults; @@ -255,3 +242,35 @@ function mergeRenderedFeatureLayers(tiles) { } return result; } + +function convertFeaturesToMapFeatures(result: QueryResults, sourceCache: SourceCache): QueryRenderedFeaturesResults { + // Merge state from SourceCache into the results + for (const layerID in result) { + for (const featureWrapper of result[layerID]) { + convertFeatureToMapFeature(featureWrapper, sourceCache); + }; + } + return result as QueryRenderedFeaturesResults; +} + +function convertFeaturesToMapFeaturesMultiple(result: QueryResults, styleLayers: {[_: string]: StyleLayer}, sourceCaches: {[_: string]: SourceCache}): QueryRenderedFeaturesResults { + // Merge state from SourceCache into the results + for (const layerName in result) { + for (const featureWrapper of result[layerName]) { + const layer = styleLayers[layerName]; + const sourceCache = sourceCaches[layer.source]; + convertFeatureToMapFeature(featureWrapper, sourceCache); + }; + } + return result as QueryRenderedFeaturesResults; +} + +function convertFeatureToMapFeature(featureWrapper: QueryResultsItem, sourceCache: SourceCache) { + const feature = featureWrapper.feature as MapGeoJSONFeature; + const state = sourceCache.getFeatureState(feature.layer['source-layer'], feature.id); + feature.source = feature.layer.source; + if (feature.layer['source-layer']) { + feature.sourceLayer = feature.layer['source-layer']; + } + feature.state = state; +} diff --git a/src/source/source_cache.ts b/src/source/source_cache.ts index d0832bf93f..5ca902d575 100644 --- a/src/source/source_cache.ts +++ b/src/source/source_cache.ts @@ -960,7 +960,7 @@ export class SourceCache extends Evented { * @param pointQueryGeometry - coordinates of the corners of bounding rectangle * @returns result items have `{tile, minX, maxX, minY, maxY}`, where min/max bounding values are the given bounds transformed in into the coordinate space of this tile. */ - tilesIn(pointQueryGeometry: Array, maxPitchScaleFactor: number, has3DLayer: boolean) { + tilesIn(pointQueryGeometry: Array, maxPitchScaleFactor: number, has3DLayer: boolean): TileResult[] { const tileResults: TileResult[] = []; const transform = this.transform; diff --git a/src/source/tile.ts b/src/source/tile.ts index 0694b9e2dc..ba183dc5cf 100644 --- a/src/source/tile.ts +++ b/src/source/tile.ts @@ -1,7 +1,6 @@ import {uniqueId, parseCacheControl} from '../util/util'; import {deserialize as deserializeBucket} from '../data/bucket'; import '../data/feature_index'; -import type {FeatureIndex} from '../data/feature_index'; import {GeoJSONFeature} from '../util/vectortile_to_geojson'; import {featureFilter} from '@maplibre/maplibre-gl-style-spec'; import {SymbolBucket} from '../data/bucket/symbol_bucket'; @@ -30,11 +29,11 @@ import type {IReadonlyTransform} from '../geo/transform_interface'; import type {LayerFeatureStates} from './source_state'; import type {FilterSpecification} from '@maplibre/maplibre-gl-style-spec'; import type Point from '@mapbox/point-geometry'; -import {type mat4} from 'gl-matrix'; +import type {mat4} from 'gl-matrix'; import type {VectorTileLayer} from '@mapbox/vector-tile'; -import {type ExpiryData} from '../util/ajax'; -import {type QueryRenderedFeaturesOptionsStrict} from './query_features'; - +import type {ExpiryData} from '../util/ajax'; +import type {QueryRenderedFeaturesOptionsStrict} from './query_features'; +import type {FeatureIndex, QueryResults} from '../data/feature_index'; /** * The tile's state, can be: * @@ -290,7 +289,7 @@ export class Tile { transform: IReadonlyTransform, maxPitchScaleFactor: number, pixelPosMatrix: mat4 - ): {[_: string]: Array<{featureIndex: number; feature: GeoJSONFeature}>} { + ): QueryResults { if (!this.latestFeatureIndex || !this.latestFeatureIndex.rawTileData) return {}; diff --git a/src/style/query_utils.ts b/src/style/query_utils.ts index cbca4596b8..02510ec3eb 100644 --- a/src/style/query_utils.ts +++ b/src/style/query_utils.ts @@ -22,11 +22,21 @@ export function translateDistance(translate: [number, number]) { return Math.sqrt(translate[0] * translate[0] + translate[1] * translate[1]); } +/** + * @internal + * Translates a geometry by a certain pixels in tile coordinates + * @param queryGeometry - The geometry to translate in tile coordinates + * @param translate - The translation in pixels + * @param translateAnchor - The anchor of the translation + * @param bearing - The bearing of the map + * @param pixelsToTileUnits - The scale factor from pixels to tile units + * @returns the translated geometry in tile coordinates + */ export function translate(queryGeometry: Array, translate: [number, number], translateAnchor: 'viewport' | 'map', bearing: number, - pixelsToTileUnits: number) { + pixelsToTileUnits: number): Point[] { if (!translate[0] && !translate[1]) { return queryGeometry; } @@ -36,7 +46,7 @@ export function translate(queryGeometry: Array, pt._rotate(-bearing); } - const translated = []; + const translated: Point[] = []; for (let i = 0; i < queryGeometry.length; i++) { const point = queryGeometry[i]; translated.push(point.sub(pt)); diff --git a/src/style/style.test.ts b/src/style/style.test.ts index 9a2329f602..e568dd3b06 100644 --- a/src/style/style.test.ts +++ b/src/style/style.test.ts @@ -18,6 +18,7 @@ import {RTLPluginLoadedEventName} from '../source/rtl_text_plugin_status'; import {MessageType} from '../util/actor_messages'; import {MercatorTransform} from '../geo/projection/mercator_transform'; import {type Tile} from '../source/tile'; +import type Point from '@mapbox/point-geometry'; function createStyleJSON(properties?): StyleSpecification { return extend({ @@ -2310,17 +2311,17 @@ describe('Style#queryRenderedFeatures', () => { }); test('returns feature type', () => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {}, transform); + const results = style.queryRenderedFeatures([{x: 0, y: 0} as Point], {}, transform); expect(results[0].geometry.type).toBe('Line'); }); test('filters by `layers` option', () => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {layers: ['land']}, transform); + const results = style.queryRenderedFeatures([{x: 0, y: 0} as Point], {layers: ['land']}, transform); expect(results).toHaveLength(2); }); test('filters by `layers` option as a Set', () => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {layers: new Set(['land'])}, transform); + const results = style.queryRenderedFeatures([{x: 0, y: 0} as Point], {layers: new Set(['land'])}, transform); expect(results).toHaveLength(2); }); @@ -2332,37 +2333,37 @@ describe('Style#queryRenderedFeatures', () => { } return style; }); - style.queryRenderedFeatures([{x: 0, y: 0}], {layers: 'string' as any}, transform); + style.queryRenderedFeatures([{x: 0, y: 0} as Point], {layers: 'string' as any}, transform); expect(errors).toBe(1); }); test('includes layout properties', () => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {}, transform); + const results = style.queryRenderedFeatures([{x: 0, y: 0} as Point], {}, transform); const layout = results[0].layer.layout; expect(layout['line-cap']).toBe('round'); }); test('includes paint properties', () => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {}, transform); + const results = style.queryRenderedFeatures([{x: 0, y: 0} as Point], {}, transform); expect(results[2].layer.paint['line-color']).toBe('red'); }); test('includes metadata', () => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {}, transform); + const results = style.queryRenderedFeatures([{x: 0, y: 0} as Point], {}, transform); const layer = results[1].layer; - expect(layer.metadata.something).toBe('else'); + expect((layer.metadata as any).something).toBe('else'); }); test('include multiple layers', () => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {layers: new Set(['land', 'landref'])}, transform); + const results = style.queryRenderedFeatures([{x: 0, y: 0} as Point], {layers: new Set(['land', 'landref'])}, transform); expect(results).toHaveLength(3); }); test('does not query sources not implicated by `layers` parameter', () => { style.sourceCaches.mapLibre.map.queryRenderedFeatures = vi.fn(); - style.queryRenderedFeatures([{x: 0, y: 0}], {layers: ['land--other']}, transform); + style.queryRenderedFeatures([{x: 0, y: 0} as Point], {layers: ['land--other']}, transform); expect(style.sourceCaches.mapLibre.map.queryRenderedFeatures).not.toHaveBeenCalled(); }); @@ -2372,7 +2373,7 @@ describe('Style#queryRenderedFeatures', () => { if (event['error'] && event['error'].message.includes('does not exist in the map\'s style and cannot be queried for features.')) errors++; return style; }); - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {layers: ['merp']}, transform); + const results = style.queryRenderedFeatures([{x: 0, y: 0} as Point], {layers: ['merp']}, transform); expect(errors).toBe(1); expect(results).toHaveLength(0); }); @@ -2460,7 +2461,7 @@ describe('Style#query*Features', () => { }); test('queryRenderedFeatures emits an error on incorrect filter', () => { - expect(style.queryRenderedFeatures([{x: 0, y: 0}], {filter: 7 as any}, transform)).toEqual([]); + expect(style.queryRenderedFeatures([{x: 0, y: 0} as Point], {filter: 7 as any}, transform)).toEqual([]); expect(onError.mock.calls[0][0].error.message).toMatch(/queryRenderedFeatures\.filter/); }); @@ -2472,7 +2473,7 @@ describe('Style#query*Features', () => { } return style; }); - style.queryRenderedFeatures([{x: 0, y: 0}], {filter: 'invalidFilter' as any, validate: false}, transform); + style.queryRenderedFeatures([{x: 0, y: 0} as Point], {filter: 'invalidFilter' as any, validate: false}, transform); expect(errors).toBe(0); }); diff --git a/src/style/style.ts b/src/style/style.ts index d65dd647ed..2a385af95e 100644 --- a/src/style/style.ts +++ b/src/style/style.ts @@ -15,7 +15,7 @@ import {browser} from '../util/browser'; import {Dispatcher} from '../util/dispatcher'; import {validateStyle, emitValidationErrors as _emitValidationErrors} from './validate_style'; import {type Source} from '../source/source'; -import {type QueryRenderedFeaturesOptions, type QueryRenderedFeaturesOptionsStrict, type QuerySourceFeatureOptions, queryRenderedFeatures, queryRenderedSymbols, querySourceFeatures} from '../source/query_features'; +import {type QueryRenderedFeaturesOptions, type QueryRenderedFeaturesOptionsStrict, type QueryRenderedFeaturesResults, type QueryRenderedFeaturesResultsItem, type QuerySourceFeatureOptions, queryRenderedFeatures, queryRenderedSymbols, querySourceFeatures} from '../source/query_features'; import {SourceCache} from '../source/source_cache'; import {type GeoJSONSource} from '../source/geojson_source'; import {latest as styleSpec, derefLayers as deref, emptyStyle, diff as diffStyles, type DiffCommand} from '@maplibre/maplibre-gl-style-spec'; @@ -27,6 +27,7 @@ import {ZoomHistory} from './zoom_history'; import {CrossTileSymbolIndex} from '../symbol/cross_tile_symbol_index'; import {validateCustomStyleLayer} from './style_layer/custom_style_layer'; import type {MapGeoJSONFeature} from '../util/vectortile_to_geojson'; +import type Point from '@mapbox/point-geometry'; // We're skipping validation errors with the `source.canvas` identifier in order // to continue to allow canvas sources to be added at runtime/updated in @@ -1353,7 +1354,7 @@ export class Style extends Evented { this._changed = true; } - _flattenAndSortRenderedFeatures(sourceResults: Array<{ [key: string]: Array<{featureIndex: number; feature: MapGeoJSONFeature}> }>) { + _flattenAndSortRenderedFeatures(sourceResults: QueryRenderedFeaturesResults[]): MapGeoJSONFeature[] { // Feature order is complicated. // The order between features in two 2D layers is always determined by layer order. // The order between features in two 3D layers is always determined by depth. @@ -1374,7 +1375,7 @@ export class Style extends Evented { const isLayer3D = layerId => this._layers[layerId].type === 'fill-extrusion'; const layerIndex = {}; - const features3D = []; + const features3D: QueryRenderedFeaturesResultsItem[] = []; for (let l = this._order.length - 1; l >= 0; l--) { const layerId = this._order[l]; if (isLayer3D(layerId)) { @@ -1391,10 +1392,10 @@ export class Style extends Evented { } features3D.sort((a, b) => { - return b.intersectionZ - a.intersectionZ; + return (b.intersectionZ as number) - (a.intersectionZ as number); }); - const features = []; + const features: MapGeoJSONFeature[] = []; for (let l = this._order.length - 1; l >= 0; l--) { const layerId = this._order[l]; @@ -1421,7 +1422,7 @@ export class Style extends Evented { return features; } - queryRenderedFeatures(queryGeometry: any, params: QueryRenderedFeaturesOptions, transform: IReadonlyTransform) { + queryRenderedFeatures(queryGeometry: Point[], params: QueryRenderedFeaturesOptions, transform: IReadonlyTransform): MapGeoJSONFeature[] { if (params && params.filter) { this._validate(validateStyle.filter, 'queryRenderedFeatures.filter', params.filter, null, params); } @@ -1444,7 +1445,7 @@ export class Style extends Evented { } } - const sourceResults = []; + const sourceResults: QueryRenderedFeaturesResults[] = []; params.availableImages = this._availableImages; diff --git a/src/style/style_layer.ts b/src/style/style_layer.ts index d28e8248e9..bb9324213c 100644 --- a/src/style/style_layer.ts +++ b/src/style/style_layer.ts @@ -28,6 +28,44 @@ import type {VectorTileFeature} from '@mapbox/vector-tile'; const TRANSITION_SUFFIX = '-transition'; +export type QueryIntersectsFeatureParams = { + /** + * The geometry to check intersection with. + * This geometry is in tile coordinates. + */ + queryGeometry: Array; + /** + * The feature to allow expression evaluation. + */ + feature: VectorTileFeature; + /** + * The feature state to allow expression evaluation. + */ + featureState: FeatureState; + /** + * The geometry of the feature. + * This geometry is in tile coordinates. + */ + geometry: Array>; + /** + * The current zoom level. + */ + zoom: number; + /** + * The transform to convert from tile coordinates to pixels. + */ + transform: IReadonlyTransform; + /** + * The number of pixels per tile unit. + */ + pixelsToTileUnits: number; + /** + * The matrix to convert from tile coordinates to pixel coordinates. + * The pixel coordinates are relative to the center of the screen. + */ + pixelPosMatrix: mat4; +}; + /** * A base class for style layers */ @@ -56,16 +94,7 @@ export abstract class StyleLayer extends Evented { readonly onRemove: ((map: Map) => void); queryRadius?(bucket: Bucket): number; - queryIntersectsFeature?( - queryGeometry: Array, - feature: VectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: IReadonlyTransform, - pixelsToTileUnits: number, - pixelPosMatrix: mat4 - ): boolean | number; + queryIntersectsFeature?(params: QueryIntersectsFeatureParams): boolean | number; constructor(layer: LayerSpecification | CustomLayerInterface, properties: Readonly<{ layout?: Properties; diff --git a/src/style/style_layer/circle_style_layer.ts b/src/style/style_layer/circle_style_layer.ts index 1be3dfcc98..a51d2ad26c 100644 --- a/src/style/style_layer/circle_style_layer.ts +++ b/src/style/style_layer/circle_style_layer.ts @@ -1,4 +1,4 @@ -import {StyleLayer} from '../style_layer'; +import {StyleLayer, type QueryIntersectsFeatureParams} from '../style_layer'; import {CircleBucket} from '../../data/bucket/circle_bucket'; import {polygonIntersectsBufferedPoint} from '../../util/intersection_tests'; @@ -7,11 +7,9 @@ import properties, {type CircleLayoutPropsPossiblyEvaluated, type CirclePaintPro import {type Transitionable, type Transitioning, type Layout, type PossiblyEvaluated} from '../properties'; import {type mat4, vec4} from 'gl-matrix'; import Point from '@mapbox/point-geometry'; -import type {FeatureState, LayerSpecification} from '@maplibre/maplibre-gl-style-spec'; -import type {IReadonlyTransform} from '../../geo/transform_interface'; +import type {LayerSpecification} from '@maplibre/maplibre-gl-style-spec'; import type {Bucket, BucketParameters} from '../../data/bucket'; import type {CircleLayoutProps, CirclePaintProps} from './circle_style_layer_properties.g'; -import type {VectorTileFeature} from '@mapbox/vector-tile'; export const isCircleStyleLayer = (layer: StyleLayer): layer is CircleStyleLayer => layer.type === 'circle'; @@ -41,15 +39,14 @@ export class CircleStyleLayer extends StyleLayer { translateDistance(this.paint.get('circle-translate')); } - queryIntersectsFeature( - queryGeometry: Array, - feature: VectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: IReadonlyTransform, - pixelsToTileUnits: number, - pixelPosMatrix: mat4 + queryIntersectsFeature({ + queryGeometry, + feature, + featureState, + geometry, + transform, + pixelsToTileUnits, + pixelPosMatrix}: QueryIntersectsFeatureParams ): boolean { const translatedPolygon = translate(queryGeometry, this.paint.get('circle-translate'), diff --git a/src/style/style_layer/fill_extrusion_style_layer.ts b/src/style/style_layer/fill_extrusion_style_layer.ts index 1ce1ab5d44..e6bb6a2810 100644 --- a/src/style/style_layer/fill_extrusion_style_layer.ts +++ b/src/style/style_layer/fill_extrusion_style_layer.ts @@ -1,4 +1,4 @@ -import {StyleLayer} from '../style_layer'; +import {type QueryIntersectsFeatureParams, StyleLayer} from '../style_layer'; import {FillExtrusionBucket} from '../../data/bucket/fill_extrusion_bucket'; import {polygonIntersectsPolygon, polygonIntersectsMultiPolygon} from '../../util/intersection_tests'; @@ -7,11 +7,9 @@ import properties, {type FillExtrusionPaintPropsPossiblyEvaluated} from './fill_ import {type Transitionable, type Transitioning, type PossiblyEvaluated} from '../properties'; import {type mat4, vec4} from 'gl-matrix'; import Point from '@mapbox/point-geometry'; -import type {FeatureState, LayerSpecification} from '@maplibre/maplibre-gl-style-spec'; +import type {LayerSpecification} from '@maplibre/maplibre-gl-style-spec'; import type {BucketParameters} from '../../data/bucket'; import type {FillExtrusionPaintProps} from './fill_extrusion_style_layer_properties.g'; -import type {IReadonlyTransform} from '../../geo/transform_interface'; -import type {VectorTileFeature} from '@mapbox/vector-tile'; export class Point3D extends Point { z: number; @@ -40,15 +38,14 @@ export class FillExtrusionStyleLayer extends StyleLayer { return true; } - queryIntersectsFeature( - queryGeometry: Array, - feature: VectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: IReadonlyTransform, - pixelsToTileUnits: number, - pixelPosMatrix: mat4 + queryIntersectsFeature({ + queryGeometry, + feature, + featureState, + geometry, + transform, + pixelsToTileUnits, + pixelPosMatrix}: QueryIntersectsFeatureParams ): boolean | number { const translatedPolygon = translate(queryGeometry, diff --git a/src/style/style_layer/fill_style_layer.ts b/src/style/style_layer/fill_style_layer.ts index 3f1ee4199b..698c2c0a1f 100644 --- a/src/style/style_layer/fill_style_layer.ts +++ b/src/style/style_layer/fill_style_layer.ts @@ -1,18 +1,14 @@ -import {StyleLayer} from '../style_layer'; - +import {type QueryIntersectsFeatureParams, StyleLayer} from '../style_layer'; import {FillBucket} from '../../data/bucket/fill_bucket'; import {polygonIntersectsMultiPolygon} from '../../util/intersection_tests'; import {translateDistance, translate} from '../query_utils'; import properties, {type FillLayoutPropsPossiblyEvaluated, type FillPaintPropsPossiblyEvaluated} from './fill_style_layer_properties.g'; -import {type Transitionable, type Transitioning, type Layout, type PossiblyEvaluated} from '../properties'; -import type {FeatureState, LayerSpecification} from '@maplibre/maplibre-gl-style-spec'; +import type {Transitionable, Transitioning, Layout, PossiblyEvaluated} from '../properties'; +import type {LayerSpecification} from '@maplibre/maplibre-gl-style-spec'; import type {BucketParameters} from '../../data/bucket'; -import type Point from '@mapbox/point-geometry'; import type {FillLayoutProps, FillPaintProps} from './fill_style_layer_properties.g'; import type {EvaluationParameters} from '../evaluation_parameters'; -import type {IReadonlyTransform} from '../../geo/transform_interface'; -import type {VectorTileFeature} from '@mapbox/vector-tile'; export const isFillStyleLayer = (layer: StyleLayer): layer is FillStyleLayer => layer.type === 'fill'; @@ -45,14 +41,11 @@ export class FillStyleLayer extends StyleLayer { return translateDistance(this.paint.get('fill-translate')); } - queryIntersectsFeature( - queryGeometry: Array, - feature: VectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: IReadonlyTransform, - pixelsToTileUnits: number + queryIntersectsFeature({ + queryGeometry, + geometry, + transform, + pixelsToTileUnits}: QueryIntersectsFeatureParams ): boolean { const translatedPolygon = translate(queryGeometry, this.paint.get('fill-translate'), diff --git a/src/style/style_layer/line_style_layer.ts b/src/style/style_layer/line_style_layer.ts index 433b6bf4fb..3d32d97ca7 100644 --- a/src/style/style_layer/line_style_layer.ts +++ b/src/style/style_layer/line_style_layer.ts @@ -1,6 +1,4 @@ -import type Point from '@mapbox/point-geometry'; - -import {StyleLayer} from '../style_layer'; +import {type QueryIntersectsFeatureParams, StyleLayer} from '../style_layer'; import {LineBucket} from '../../data/bucket/line_bucket'; import {polygonIntersectsBufferedMultiLine} from '../../util/intersection_tests'; import {getMaximumPaintValue, translateDistance, translate, offsetLine} from '../query_utils'; @@ -10,11 +8,9 @@ import {EvaluationParameters} from '../evaluation_parameters'; import {type Transitionable, type Transitioning, type Layout, type PossiblyEvaluated, DataDrivenProperty} from '../properties'; import {isZoomExpression, Step} from '@maplibre/maplibre-gl-style-spec'; -import type {FeatureState, LayerSpecification} from '@maplibre/maplibre-gl-style-spec'; +import type {LayerSpecification} from '@maplibre/maplibre-gl-style-spec'; import type {Bucket, BucketParameters} from '../../data/bucket'; import type {LineLayoutProps, LinePaintProps} from './line_style_layer_properties.g'; -import type {IReadonlyTransform} from '../../geo/transform_interface'; -import type {VectorTileFeature} from '@mapbox/vector-tile'; export class LineFloorwidthProperty extends DataDrivenProperty { useIntegerZoom: true; @@ -95,14 +91,13 @@ export class LineStyleLayer extends StyleLayer { return width / 2 + Math.abs(offset) + translateDistance(this.paint.get('line-translate')); } - queryIntersectsFeature( - queryGeometry: Array, - feature: VectorTileFeature, - featureState: FeatureState, - geometry: Array>, - zoom: number, - transform: IReadonlyTransform, - pixelsToTileUnits: number + queryIntersectsFeature({ + queryGeometry, + feature, + featureState, + geometry, + transform, + pixelsToTileUnits}: QueryIntersectsFeatureParams ): boolean { const translatedPolygon = translate(queryGeometry, this.paint.get('line-translate'), diff --git a/src/ui/map.ts b/src/ui/map.ts index 04bef24227..6f0369417a 100644 --- a/src/ui/map.ts +++ b/src/ui/map.ts @@ -57,7 +57,7 @@ import type { SkySpecification } from '@maplibre/maplibre-gl-style-spec'; import type {CanvasSourceSpecification} from '../source/canvas_source'; -import type {MapGeoJSONFeature} from '../util/vectortile_to_geojson'; +import type {GeoJSONFeature, MapGeoJSONFeature} from '../util/vectortile_to_geojson'; import type {ControlPosition, IControl} from './control/control'; import type {QueryRenderedFeaturesOptions, QuerySourceFeatureOptions} from '../source/query_features'; import {MercatorTransform} from '../geo/projection/mercator_transform'; @@ -1724,7 +1724,7 @@ export class Map extends Camera { if (!this.style) { return []; } - let queryGeometry; + let queryGeometry: Point[]; const isGeometry = geometryOrOptions instanceof Point || Array.isArray(geometryOrOptions); const geometry = isGeometry ? geometryOrOptions : [[0, 0], [this.transform.width, this.transform.height]]; options = options || (isGeometry ? {} : geometryOrOptions) || {}; @@ -1770,7 +1770,7 @@ export class Map extends Camera { * ``` * */ - querySourceFeatures(sourceId: string, parameters?: QuerySourceFeatureOptions | null): MapGeoJSONFeature[] { + querySourceFeatures(sourceId: string, parameters?: QuerySourceFeatureOptions | null): GeoJSONFeature[] { return this.style.querySourceFeatures(sourceId, parameters); } From 7eed732f27940538a04d6ab4f30bc76e9ace7b69 Mon Sep 17 00:00:00 2001 From: HarelM Date: Mon, 30 Dec 2024 13:48:59 +0200 Subject: [PATCH 3/6] Fix lint --- src/data/feature_index.ts | 4 ++-- src/source/query_features.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data/feature_index.ts b/src/data/feature_index.ts index 41a761d0b6..d9282fd5bd 100644 --- a/src/data/feature_index.ts +++ b/src/data/feature_index.ts @@ -41,13 +41,13 @@ type QueryParameters = { export type QueryResults = { [_: string]: QueryResultsItem[]; -} +}; export type QueryResultsItem = { featureIndex: number; feature: GeoJSONFeature; intersectionZ?: boolean | number; -} +}; /** * An in memory index class to allow fast interaction with features diff --git a/src/source/query_features.ts b/src/source/query_features.ts index 7202421618..7cc15ce6a1 100644 --- a/src/source/query_features.ts +++ b/src/source/query_features.ts @@ -13,7 +13,7 @@ import type {OverscaledTileID} from './tile_id'; type RenderedFeatureLayer = { wrappedTileID: string; queryResults: QueryResults; -} +}; /** * Options to pass to query the map for the rendered features @@ -64,7 +64,7 @@ export type QuerySourceFeatureOptions = { export type QueryRenderedFeaturesResults = { [key: string]: QueryRenderedFeaturesResultsItem[]; -} +}; export type QueryRenderedFeaturesResultsItem = QueryResultsItem & { feature: MapGeoJSONFeature }; From fd9dcaf44599852fe523a31f94e00e329ab82401 Mon Sep 17 00:00:00 2001 From: HarelM Date: Mon, 30 Dec 2024 14:01:51 +0200 Subject: [PATCH 4/6] Add more types, remove unwrapped call. --- src/source/query_features.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/source/query_features.ts b/src/source/query_features.ts index 7cc15ce6a1..22dd8a98d9 100644 --- a/src/source/query_features.ts +++ b/src/source/query_features.ts @@ -71,11 +71,11 @@ export type QueryRenderedFeaturesResultsItem = QueryResultsItem & { feature: Map /* * Returns a matrix that can be used to convert from tile coordinates to viewport pixel coordinates. */ -function getPixelPosMatrix(transform: IReadonlyTransform, tileID) { +function getPixelPosMatrix(transform: IReadonlyTransform, tileID: OverscaledTileID) { const t = mat4.create(); mat4.translate(t, t, [1, 1, 0]); mat4.scale(t, t, [transform.width * 0.5, transform.height * 0.5, 1]); - const projectionData = transform.getProjectionData({overscaledTileID: tileID.toUnwrapped()}); + const projectionData = transform.getProjectionData({overscaledTileID: tileID}); return mat4.multiply(t, t, projectionData.mainMatrix); } From bfc58c00ec32bf2f2f00d75d0230212cf0a0cb68 Mon Sep 17 00:00:00 2001 From: Harel M Date: Tue, 31 Dec 2024 13:41:51 +0200 Subject: [PATCH 5/6] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da71636dea..4ec51e365f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### ✨ Features and improvements - Allows setting the desired WebGL version to use ([#5236](https://github.com/maplibre/maplibre-gl-js/pull/5236)). You can now use `contextType` inside `canvasContextAttributes` to choose which WebGL version to use +- ⚠️ `StyleLayer`'s `queryIntersectsFeature` method parameters were moved to `QueryIntersectsFeatureParams`. To overcome it simply wrap the method parameters with `{}` ([#5276](https://github.com/maplibre/maplibre-gl-js/pull/5276)) - _...Add new stuff here..._ ### 🐞 Bug fixes From fe404dce62a92cde0fa61b8f5b1eee11deb1fb95 Mon Sep 17 00:00:00 2001 From: Harel M Date: Tue, 31 Dec 2024 14:05:00 +0200 Subject: [PATCH 6/6] Update query_features.ts --- src/source/query_features.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/source/query_features.ts b/src/source/query_features.ts index 22dd8a98d9..821edf0025 100644 --- a/src/source/query_features.ts +++ b/src/source/query_features.ts @@ -71,12 +71,15 @@ export type QueryRenderedFeaturesResultsItem = QueryResultsItem & { feature: Map /* * Returns a matrix that can be used to convert from tile coordinates to viewport pixel coordinates. */ -function getPixelPosMatrix(transform: IReadonlyTransform, tileID: OverscaledTileID) { +function getPixelPosMatrix(transform, tileID: OverscaledTileID) { const t = mat4.create(); mat4.translate(t, t, [1, 1, 0]); mat4.scale(t, t, [transform.width * 0.5, transform.height * 0.5, 1]); - const projectionData = transform.getProjectionData({overscaledTileID: tileID}); - return mat4.multiply(t, t, projectionData.mainMatrix); + if (transform.calculatePosMatrix) { // Globe: TODO: remove this hack once queryRendererFeatures supports globe properly + return mat4.multiply(t, t, transform.calculatePosMatrix(tileID.toUnwrapped())); + } else { + return t; + } } function queryIncludes3DLayer(layers: Set | undefined, styleLayers: {[_: string]: StyleLayer}, sourceID: string) {