Skip to content

Commit

Permalink
Improve typing and change StyleLayer query call interface (#5276)
Browse files Browse the repository at this point in the history
* Initial basic refactoring

* Added more types to better understand what's going on...

* Fix lint

* Add more types, remove unwrapped call.

* Update CHANGELOG.md

* Update query_features.ts
  • Loading branch information
HarelM authored Dec 31, 2024
1 parent 4e5a9fe commit c2725d8
Show file tree
Hide file tree
Showing 14 changed files with 210 additions and 155 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 25 additions & 12 deletions src/data/feature_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
Expand All @@ -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];
Expand All @@ -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
});
}
);
}
Expand All @@ -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,
Expand Down Expand Up @@ -261,8 +274,8 @@ export class FeatureIndex {
filterSpec: FilterSpecification,
filterLayerIDs: Set<string> | null,
availableImages: Array<string>,
styleLayers: {[_: string]: StyleLayer}) {
const result = {};
styleLayers: {[_: string]: StyleLayer}): QueryResults {
const result: QueryResults = {};
this.loadVTLayers();

const filter = featureFilter(filterSpec);
Expand Down
101 changes: 60 additions & 41 deletions src/source/query_features.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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, 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]);
Expand Down Expand Up @@ -95,14 +108,14 @@ export function queryRenderedFeatures(
queryGeometry: Array<Point>,
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,
Expand All @@ -122,19 +135,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},
Expand All @@ -145,8 +146,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)) {
Expand Down Expand Up @@ -192,29 +193,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++) {
Expand All @@ -229,16 +216,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;
Expand All @@ -258,3 +245,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;
}
2 changes: 1 addition & 1 deletion src/source/source_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Point>, maxPitchScaleFactor: number, has3DLayer: boolean) {
tilesIn(pointQueryGeometry: Array<Point>, maxPitchScaleFactor: number, has3DLayer: boolean): TileResult[] {
const tileResults: TileResult[] = [];

const transform = this.transform;
Expand Down
11 changes: 5 additions & 6 deletions src/source/tile.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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:
*
Expand Down Expand Up @@ -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 {};

Expand Down
14 changes: 12 additions & 2 deletions src/style/query_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Point>,
translate: [number, number],
translateAnchor: 'viewport' | 'map',
bearing: number,
pixelsToTileUnits: number) {
pixelsToTileUnits: number): Point[] {
if (!translate[0] && !translate[1]) {
return queryGeometry;
}
Expand All @@ -36,7 +46,7 @@ export function translate(queryGeometry: Array<Point>,
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));
Expand Down
Loading

0 comments on commit c2725d8

Please sign in to comment.