Skip to content

Commit b86a1ab

Browse files
stepankuzminmourner
authored andcommitted
[GLJS-1062] Improve queryRenderedFeatures support in the Interactions API (internal-1945)
1 parent c1e8947 commit b86a1ab

20 files changed

+892
-491
lines changed

3d-style/style/style_layer/model_style_layer.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import EXTENT from '../../../src/style-spec/data/extent';
1010
import {convertModelMatrixForGlobe, queryGeometryIntersectsProjectedAabb} from '../../util/model_util';
1111
import Tiled3dModelBucket from '../../data/bucket/tiled_3d_model_bucket';
1212
import EvaluationParameters from '../../../src/style/evaluation_parameters';
13+
import Feature from '../../../src/util/vectortile_to_geojson';
1314

1415
import type {vec3} from 'gl-matrix';
1516
import type {Transitionable, Transitioning, PossiblyEvaluated, PropertyValue, ConfigOptions} from '../../../src/style/properties';
@@ -24,7 +25,6 @@ import type ModelManager from '../../render/model_manager';
2425
import type {Node} from '../../data/model';
2526
import type {VectorTileFeature} from '@mapbox/vector-tile';
2627
import type {FeatureFilter} from '../../../src/style-spec/feature_filter/index';
27-
import type Feature from '../../../src/util/vectortile_to_geojson';
2828
import type {CanonicalTileID} from '../../../src/source/tile_id';
2929
import type {LUT} from "../../../src/util/lut";
3030

@@ -239,14 +239,15 @@ class ModelStyleLayer extends StyleLayer {
239239

240240
const position = new LngLat(0, 0);
241241
tileToLngLat(tile.tileID.canonical, position, nodeInfo.node.anchor[0], nodeInfo.node.anchor[1]);
242-
queryFeature = {
243-
type: 'Feature',
244-
geometry: {type: "Point", coordinates: [position.lng, position.lat]},
245-
properties: nodeInfo.feature.properties,
246-
id: nodeInfo.feature.id,
247-
state: {}, // append later
248-
layer: this.serialize()
249-
};
242+
243+
const {z, x, y} = tile.tileID.canonical;
244+
queryFeature = new Feature({} as unknown as VectorTileFeature, z, x, y, nodeInfo.feature.id);
245+
queryFeature.properties = nodeInfo.feature.properties;
246+
queryFeature.geometry = {type: 'Point', coordinates: [position.lng, position.lat]};
247+
queryFeature.layer = {...this.serialize(), id: this.fqid};
248+
queryFeature.state = {};
249+
queryFeature.tile = tile.tileID.canonical;
250+
250251
return {queryFeature, intersectionZ};
251252
}
252253
}

debug/featuresets.html

+55-105
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<title>Mapbox GL JS debug page</title>
55
<meta charset='utf-8'>
6-
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6+
<meta name='viewport' content='width=device-width, initial-scale=1.0, user-scalable=no'>
77
<link rel='stylesheet' href='../dist/mapbox-gl.css' />
88
<style>
99
body { margin: 0; padding: 0; }
@@ -20,75 +20,76 @@
2020

2121
var map = window.map = new mapboxgl.Map({
2222
container: 'map',
23-
devtools: true,
24-
zoom: 12.5,
25-
center: [-122.4194, 37.7749],
23+
zoom: 16.7,
24+
center: [24.9425, 60.1715],
25+
pitch: 67,
26+
bearing: -34,
2627
hash: true,
2728
style: 'mapbox://styles/mapbox-map-design/standard-experimental-ime',
2829
});
2930

30-
31-
// Selecting buildings
32-
var selectedBuildings = [];
31+
// Selecting Buildings
32+
let selectedBuildings = [];
3333
map.addInteraction('building-click', {
3434
type: 'click',
35-
featureset: {featuresetId: "buildings", importId: "basemap"},
35+
featureset: {featuresetId: 'buildings', importId: 'basemap'},
3636
handler: (e) => {
37-
// Clear selected building
38-
selectedBuildings.forEach(f => map.setFeatureState(f, {select: false}));
39-
40-
map.setFeatureState(e.feature, {select: true});
41-
selectedBuildings = [e.feature];
37+
map.setFeatureState(e.feature, {select: !e.feature.state.select});
38+
selectedBuildings.push(e.feature);
4239
}
4340
});
4441

4542
// Selecting POIs
46-
var selectedPoi = null;
47-
const selectedPoiMarker = new mapboxgl.Marker();
48-
selectedPoiMarker.getElement().style.cursor = 'pointer';
43+
let selectedPoi = null;
44+
const poiMarker = new mapboxgl.Marker({color: 'red'});
45+
poiMarker.getElement().style.cursor = 'pointer';
46+
4947
map.addInteraction('poi-click', {
5048
type: 'click',
51-
featureset: {featuresetId: "poi", importId: "basemap"},
49+
featureset: {featuresetId: 'poi', importId: 'basemap'},
5250
handler: (e) => {
53-
console.log("poi click", e.feature);
54-
if(selectedPoi) {
51+
if (selectedPoi) {
5552
map.setFeatureState(selectedPoi, {hide: false});
56-
selectedPoiMarker.remove();
53+
poiMarker.remove();
5754
}
5855

5956
selectedPoi = e.feature;
60-
selectedPoiMarker
61-
.setLngLat(e.feature.geometry.coordinates)
57+
poiMarker
58+
.setLngLat(selectedPoi.geometry.coordinates)
6259
.addTo(map);
6360

6461
let html = '';
6562
for (const key in e.feature.properties) {
6663
html += `<div><b>${key}</b>: ${e.feature.properties[key]}</div>`;
6764
}
68-
selectedPoiMarker.setPopup(new mapboxgl.Popup().setHTML(html));
65+
66+
const popup = new mapboxgl.Popup().setHTML(html);
67+
poiMarker.setPopup(popup);
6968

7069
map.setFeatureState(e.feature, {hide: true});
7170

72-
/// Optional: Highlight buildins underneath the selected pin.
73-
// TODO: Uncomment after GLJS-1062
74-
// let buildings = map.queryRenderedFeatures({
75-
// featureset: {featuresetId: "buildings", importId: "basemap"},
76-
// filter: ["<=", ["distance", e.feature.geometry], 0]
77-
// })
78-
// buildings.forEach(f => map.setFeatureState(f, {select: true}));
79-
// selectedBuildings = buildings;
71+
// Highlight buildins underneath the selected pin.
72+
const buildings = map.queryRenderedFeatures({
73+
featureset: {featuresetId: 'buildings', importId: 'basemap'},
74+
filter: ['<=', ['distance', e.feature.geometry], 0]
75+
});
76+
77+
if (buildings.length > 0) {
78+
selectedBuildings.forEach(f => map.setFeatureState(f, {select: false}));
79+
buildings.forEach(f => map.setFeatureState(f, {select: true}));
80+
selectedBuildings = buildings;
81+
}
8082
}
8183
});
8284

83-
// Selecting places
84-
var selectedPlace = null;
85-
var placePopup = new mapboxgl.Popup();
85+
// Selecting Places
86+
let selectedPlace = null;
87+
let placePopup = new mapboxgl.Popup();
8688
map.addInteraction('place-click', {
8789
type: 'click',
88-
featureset: {featuresetId: "place-labels", importId: "basemap"},
90+
featureset: {featuresetId: 'place-labels', importId: 'basemap'},
8991
handler: (e) => {
90-
console.log("place click", e.feature);
91-
if(selectedPlace) {
92+
if (selectedPlace) {
9293
map.setFeatureState(selectedPlace, {select: false});
9394
}
9495

@@ -99,117 +100,66 @@
99100
for (const key in e.feature.properties) {
100101
html += `<div><b>${key}</b>: ${e.feature.properties[key]}</div>`;
101102
}
103+
102104
placePopup
103105
.setLngLat(e.feature.geometry.coordinates)
104106
.setHTML(html)
105107
.addTo(map);
106108
}
107109
});
108110

109-
// Cleaning selected features
111+
// Clearing features selection
110112
map.addInteraction('map-click', {
111113
type: 'click',
112114
handler: (e) => {
113115
// Clear selected POI
114-
if(selectedPoi) {
116+
if (selectedPoi) {
115117
map.setFeatureState(selectedPoi, {hide: false});
116-
selectedPoiMarker.remove();
118+
poiMarker.remove();
117119
selectedPoi = null;
118120
}
119121

120122
// Clear selected building
121123
selectedBuildings.forEach(f => map.setFeatureState(f, {select: false}));
122124

123125
// Clear selected place
124-
if(selectedPlace) {
126+
if (selectedPlace) {
125127
map.setFeatureState(selectedPlace, {select: false});
126128
selectedPlace = null;
127129
}
130+
128131
placePopup.remove();
129132
return false;
130133
}
131134
});
132135

133136
// Hover effects
134137

135-
var hoveredBuilding = null;
136-
var hoveredPlace = null;
137-
138-
function highlight(feature, hovered) {
139-
if (hovered) {
140-
map.setFeatureState(feature, {highlight: true});
141-
} else {
142-
map.setFeatureState(feature, {highlight: false});
143-
}
144-
}
145-
146-
map.addInteraction('building-hover', {
138+
let hoveredBuilding;
139+
map.addInteraction('building-mousemove', {
147140
type: 'mousemove',
148-
featureset: {featuresetId: "buildings", importId: "basemap"},
141+
featureset: {featuresetId: 'buildings', importId: 'basemap'},
149142
handler: (e) => {
150143
if (hoveredBuilding) {
151-
if(hoveredBuilding.id === e.feature.id && hoveredBuilding.namespace === e.feature.namespace) {
152-
// Hovering the same building
153-
return;
154-
}
144+
// Hovering the same building
145+
if(hoveredBuilding.id === e.feature.id && hoveredBuilding.namespace === e.feature.namespace) return;
155146
// Clear the old building highlight
156-
highlight(hoveredBuilding, false);
157-
}
158-
159-
if (hoveredPlace) {
160-
// Clear the place highlight
161-
highlight(hoveredPlace, false);
162-
hoveredBuilding = null;
147+
map.setFeatureState(hoveredBuilding, {highlight: false});
163148
}
164149

165150
hoveredBuilding = e.feature;
166-
highlight(hoveredBuilding, true);
167-
map.getCanvas().style.cursor = 'pointer';
168-
}
169-
});
170-
171-
map.addInteraction('place-hover', {
172-
type: 'mousemove',
173-
featureset: {featuresetId: "place-labels", importId: "basemap"},
174-
handler: (e) => {
175-
if (hoveredPlace) {
176-
if(hoveredPlace.id === e.feature.id && hoveredPlace.namespace === e.feature.namespace) {
177-
// Hovering the same place
178-
return;
179-
}
180-
// Clear the old place highlight
181-
highlight(hoveredPlace, false);
182-
}
183-
184-
if (hoveredBuilding) {
185-
// Clear the building highlight
186-
highlight(hoveredBuilding, false);
187-
hoveredBuilding = null;
188-
}
189-
190-
hoveredPlace = e.feature;
191-
highlight(hoveredPlace, true);
192-
map.getCanvas().style.cursor = 'pointer';
151+
map.setFeatureState(e.feature, {highlight: true});
193152
}
194153
});
195154

196-
// Mousemove on map that wasn't handled any featureset-specific interaction
197-
// means the mouse is moved outside of hovered features.
198-
// Thid is a safe place to clear the hover effect.
199-
map.addInteraction(`map-mousemove`, {
200-
type: 'mousemove',
155+
map.addInteraction('building-mouseleave', {
156+
type: 'mouseleave',
157+
featureset: {featuresetId: 'buildings', importId: 'basemap'},
201158
handler: (e) => {
202159
if (hoveredBuilding) {
203-
highlight(hoveredBuilding, false);
160+
map.setFeatureState(hoveredBuilding, {highlight: false});
204161
hoveredBuilding = null;
205162
}
206-
207-
if (hoveredPlace) {
208-
highlight(hoveredPlace, false);
209-
hoveredPlace = null;
210-
}
211-
map.getCanvas().style.cursor = '';
212-
return false; // Don't stop the event propagation.
213163
}
214164
});
215165

src/data/feature_index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ class FeatureIndex {
253253
featureState = sourceFeatureState.getState(styleLayer.sourceLayer || '_geojsonTileLayer', id);
254254
}
255255

256-
const intersectionZ = !intersectionTest || intersectionTest(feature, styleLayer, featureState, layoutVertexArrayOffset);
256+
const intersectionZ = (!intersectionTest || intersectionTest(feature, styleLayer, featureState, layoutVertexArrayOffset)) as number;
257257
if (!intersectionZ) {
258258
// Only applied for non-symbol features
259259
continue;
@@ -275,7 +275,7 @@ class FeatureIndex {
275275
}
276276
}
277277

278-
appendToResult(result: QueryResult, layerID: string, featureIndex: number, geojsonFeature: Feature, intersectionZ: boolean | number) {
278+
appendToResult(result: QueryResult, layerID: string, featureIndex: number, geojsonFeature: Feature, intersectionZ?: number) {
279279
let layerResult = result[layerID];
280280
if (layerResult === undefined) {
281281
layerResult = result[layerID] = [];

src/source/query_features.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type QueryResult = {
1717
[_: string]: Array<{
1818
featureIndex: number;
1919
feature: Feature;
20-
intersectionZ: boolean | number;
20+
intersectionZ: number;
2121
}>;
2222
};
2323

@@ -39,9 +39,7 @@ function getPixelPosMatrix(transform: Transform, tileID: OverscaledTileID) {
3939

4040
export function queryRenderedFeatures(
4141
sourceCache: SourceCache,
42-
styleLayers: {
43-
[_: string]: StyleLayer;
44-
},
42+
styleLayers: Record<string, StyleLayer>,
4543
queryGeometry: QueryGeometry,
4644
filter: FilterSpecification,
4745
layers: string[],

src/source/tile.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,7 @@ class Tile {
451451
// Queries non-symbol features rendered for this tile.
452452
// Symbol features are queried globally
453453
queryRenderedFeatures(
454-
layers: {
455-
[_: string]: StyleLayer;
456-
},
454+
layers: Record<string, StyleLayer>,
457455
sourceFeatureState: SourceFeatureState,
458456
tileResult: TilespaceQueryGeometry,
459457
filter: FilterSpecification,

src/style-spec/reference/v8.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@
204204
"featuresets": {
205205
"experimental": true,
206206
"type": "featuresets",
207-
"doc": "Defines sets of features for querying, interaction, and feature state manipulation.",
207+
"doc": "Defines sets of features for querying, interaction, and state management on the map, referencing individual layers or subsets of layers within the map's style.",
208208
"example": {
209209
"poi": {
210210
"selectors": [

src/style/query_geometry.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,13 @@ export class QueryGeometry {
6969
let aboveHorizon;
7070

7171
if (geometry instanceof Point || typeof geometry[0] === 'number') {
72-
const pt = Point.convert(geometry);
72+
const pt = Point.convert(geometry) as Point;
7373
screenGeometry = [pt];
74-
// @ts-expect-error - TS2345 - Argument of type 'Point | [PointLike, PointLike]' is not assignable to parameter of type 'Point'.
7574
aboveHorizon = transform.isPointAboveHorizon(pt);
7675
} else {
7776
const tl = Point.convert(geometry[0]);
78-
const br = Point.convert(geometry[1]);
77+
const br = Point.convert(geometry[1]) as Point;
7978
screenGeometry = [tl, br];
80-
// @ts-expect-error - TS2345 - Argument of type 'number | Point' is not assignable to parameter of type 'Point'.
8179
aboveHorizon = polygonizeBounds(tl, br).every((p) => transform.isPointAboveHorizon(p));
8280
}
8381

0 commit comments

Comments
 (0)