Skip to content

Commit

Permalink
[GLJS-1062] Improve queryRenderedFeatures support in the Interactio…
Browse files Browse the repository at this point in the history
…ns API (internal-1945)
  • Loading branch information
stepankuzmin authored and mourner committed Nov 12, 2024
1 parent c1e8947 commit b86a1ab
Show file tree
Hide file tree
Showing 20 changed files with 892 additions and 491 deletions.
19 changes: 10 additions & 9 deletions 3d-style/style/style_layer/model_style_layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import EXTENT from '../../../src/style-spec/data/extent';
import {convertModelMatrixForGlobe, queryGeometryIntersectsProjectedAabb} from '../../util/model_util';
import Tiled3dModelBucket from '../../data/bucket/tiled_3d_model_bucket';
import EvaluationParameters from '../../../src/style/evaluation_parameters';
import Feature from '../../../src/util/vectortile_to_geojson';

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

Expand Down Expand Up @@ -239,14 +239,15 @@ class ModelStyleLayer extends StyleLayer {

const position = new LngLat(0, 0);
tileToLngLat(tile.tileID.canonical, position, nodeInfo.node.anchor[0], nodeInfo.node.anchor[1]);
queryFeature = {
type: 'Feature',
geometry: {type: "Point", coordinates: [position.lng, position.lat]},
properties: nodeInfo.feature.properties,
id: nodeInfo.feature.id,
state: {}, // append later
layer: this.serialize()
};

const {z, x, y} = tile.tileID.canonical;
queryFeature = new Feature({} as unknown as VectorTileFeature, z, x, y, nodeInfo.feature.id);
queryFeature.properties = nodeInfo.feature.properties;
queryFeature.geometry = {type: 'Point', coordinates: [position.lng, position.lat]};
queryFeature.layer = {...this.serialize(), id: this.fqid};
queryFeature.state = {};
queryFeature.tile = tile.tileID.canonical;

return {queryFeature, intersectionZ};
}
}
Expand Down
160 changes: 55 additions & 105 deletions debug/featuresets.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<title>Mapbox GL JS debug page</title>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name='viewport' content='width=device-width, initial-scale=1.0, user-scalable=no'>
<link rel='stylesheet' href='../dist/mapbox-gl.css' />
<style>
body { margin: 0; padding: 0; }
Expand All @@ -20,75 +20,76 @@

var map = window.map = new mapboxgl.Map({
container: 'map',
devtools: true,
zoom: 12.5,
center: [-122.4194, 37.7749],
zoom: 16.7,
center: [24.9425, 60.1715],
pitch: 67,
bearing: -34,
hash: true,
style: 'mapbox://styles/mapbox-map-design/standard-experimental-ime',
});


// Selecting buildings
var selectedBuildings = [];
// Selecting Buildings
let selectedBuildings = [];
map.addInteraction('building-click', {
type: 'click',
featureset: {featuresetId: "buildings", importId: "basemap"},
featureset: {featuresetId: 'buildings', importId: 'basemap'},
handler: (e) => {
// Clear selected building
selectedBuildings.forEach(f => map.setFeatureState(f, {select: false}));

map.setFeatureState(e.feature, {select: true});
selectedBuildings = [e.feature];
map.setFeatureState(e.feature, {select: !e.feature.state.select});
selectedBuildings.push(e.feature);
}
});

// Selecting POIs
var selectedPoi = null;
const selectedPoiMarker = new mapboxgl.Marker();
selectedPoiMarker.getElement().style.cursor = 'pointer';
let selectedPoi = null;
const poiMarker = new mapboxgl.Marker({color: 'red'});
poiMarker.getElement().style.cursor = 'pointer';

map.addInteraction('poi-click', {
type: 'click',
featureset: {featuresetId: "poi", importId: "basemap"},
featureset: {featuresetId: 'poi', importId: 'basemap'},
handler: (e) => {
console.log("poi click", e.feature);
if(selectedPoi) {
if (selectedPoi) {
map.setFeatureState(selectedPoi, {hide: false});
selectedPoiMarker.remove();
poiMarker.remove();
}

selectedPoi = e.feature;
selectedPoiMarker
.setLngLat(e.feature.geometry.coordinates)
poiMarker
.setLngLat(selectedPoi.geometry.coordinates)
.addTo(map);

let html = '';
for (const key in e.feature.properties) {
html += `<div><b>${key}</b>: ${e.feature.properties[key]}</div>`;
}
selectedPoiMarker.setPopup(new mapboxgl.Popup().setHTML(html));

const popup = new mapboxgl.Popup().setHTML(html);
poiMarker.setPopup(popup);

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

/// Optional: Highlight buildins underneath the selected pin.
// TODO: Uncomment after GLJS-1062
// let buildings = map.queryRenderedFeatures({
// featureset: {featuresetId: "buildings", importId: "basemap"},
// filter: ["<=", ["distance", e.feature.geometry], 0]
// })
// buildings.forEach(f => map.setFeatureState(f, {select: true}));
// selectedBuildings = buildings;
// Highlight buildins underneath the selected pin.
const buildings = map.queryRenderedFeatures({
featureset: {featuresetId: 'buildings', importId: 'basemap'},
filter: ['<=', ['distance', e.feature.geometry], 0]
});

if (buildings.length > 0) {
selectedBuildings.forEach(f => map.setFeatureState(f, {select: false}));
buildings.forEach(f => map.setFeatureState(f, {select: true}));
selectedBuildings = buildings;
}
}
});

// Selecting places
var selectedPlace = null;
var placePopup = new mapboxgl.Popup();
// Selecting Places
let selectedPlace = null;
let placePopup = new mapboxgl.Popup();
map.addInteraction('place-click', {
type: 'click',
featureset: {featuresetId: "place-labels", importId: "basemap"},
featureset: {featuresetId: 'place-labels', importId: 'basemap'},
handler: (e) => {
console.log("place click", e.feature);
if(selectedPlace) {
if (selectedPlace) {
map.setFeatureState(selectedPlace, {select: false});
}

Expand All @@ -99,117 +100,66 @@
for (const key in e.feature.properties) {
html += `<div><b>${key}</b>: ${e.feature.properties[key]}</div>`;
}

placePopup
.setLngLat(e.feature.geometry.coordinates)
.setHTML(html)
.addTo(map);
}
});

// Cleaning selected features
// Clearing features selection
map.addInteraction('map-click', {
type: 'click',
handler: (e) => {
// Clear selected POI
if(selectedPoi) {
if (selectedPoi) {
map.setFeatureState(selectedPoi, {hide: false});
selectedPoiMarker.remove();
poiMarker.remove();
selectedPoi = null;
}

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

// Clear selected place
if(selectedPlace) {
if (selectedPlace) {
map.setFeatureState(selectedPlace, {select: false});
selectedPlace = null;
}

placePopup.remove();
return false;
}
});

// Hover effects

var hoveredBuilding = null;
var hoveredPlace = null;

function highlight(feature, hovered) {
if (hovered) {
map.setFeatureState(feature, {highlight: true});
} else {
map.setFeatureState(feature, {highlight: false});
}
}

map.addInteraction('building-hover', {
let hoveredBuilding;
map.addInteraction('building-mousemove', {
type: 'mousemove',
featureset: {featuresetId: "buildings", importId: "basemap"},
featureset: {featuresetId: 'buildings', importId: 'basemap'},
handler: (e) => {
if (hoveredBuilding) {
if(hoveredBuilding.id === e.feature.id && hoveredBuilding.namespace === e.feature.namespace) {
// Hovering the same building
return;
}
// Hovering the same building
if(hoveredBuilding.id === e.feature.id && hoveredBuilding.namespace === e.feature.namespace) return;
// Clear the old building highlight
highlight(hoveredBuilding, false);
}

if (hoveredPlace) {
// Clear the place highlight
highlight(hoveredPlace, false);
hoveredBuilding = null;
map.setFeatureState(hoveredBuilding, {highlight: false});
}

hoveredBuilding = e.feature;
highlight(hoveredBuilding, true);
map.getCanvas().style.cursor = 'pointer';
}
});

map.addInteraction('place-hover', {
type: 'mousemove',
featureset: {featuresetId: "place-labels", importId: "basemap"},
handler: (e) => {
if (hoveredPlace) {
if(hoveredPlace.id === e.feature.id && hoveredPlace.namespace === e.feature.namespace) {
// Hovering the same place
return;
}
// Clear the old place highlight
highlight(hoveredPlace, false);
}

if (hoveredBuilding) {
// Clear the building highlight
highlight(hoveredBuilding, false);
hoveredBuilding = null;
}

hoveredPlace = e.feature;
highlight(hoveredPlace, true);
map.getCanvas().style.cursor = 'pointer';
map.setFeatureState(e.feature, {highlight: true});
}
});

// Mousemove on map that wasn't handled any featureset-specific interaction
// means the mouse is moved outside of hovered features.
// Thid is a safe place to clear the hover effect.
map.addInteraction(`map-mousemove`, {
type: 'mousemove',
map.addInteraction('building-mouseleave', {
type: 'mouseleave',
featureset: {featuresetId: 'buildings', importId: 'basemap'},
handler: (e) => {
if (hoveredBuilding) {
highlight(hoveredBuilding, false);
map.setFeatureState(hoveredBuilding, {highlight: false});
hoveredBuilding = null;
}

if (hoveredPlace) {
highlight(hoveredPlace, false);
hoveredPlace = null;
}
map.getCanvas().style.cursor = '';
return false; // Don't stop the event propagation.
}
});

Expand Down
4 changes: 2 additions & 2 deletions src/data/feature_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ class FeatureIndex {
featureState = sourceFeatureState.getState(styleLayer.sourceLayer || '_geojsonTileLayer', id);
}

const intersectionZ = !intersectionTest || intersectionTest(feature, styleLayer, featureState, layoutVertexArrayOffset);
const intersectionZ = (!intersectionTest || intersectionTest(feature, styleLayer, featureState, layoutVertexArrayOffset)) as number;
if (!intersectionZ) {
// Only applied for non-symbol features
continue;
Expand All @@ -275,7 +275,7 @@ class FeatureIndex {
}
}

appendToResult(result: QueryResult, layerID: string, featureIndex: number, geojsonFeature: Feature, intersectionZ: boolean | number) {
appendToResult(result: QueryResult, layerID: string, featureIndex: number, geojsonFeature: Feature, intersectionZ?: number) {
let layerResult = result[layerID];
if (layerResult === undefined) {
layerResult = result[layerID] = [];
Expand Down
6 changes: 2 additions & 4 deletions src/source/query_features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type QueryResult = {
[_: string]: Array<{
featureIndex: number;
feature: Feature;
intersectionZ: boolean | number;
intersectionZ: number;
}>;
};

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

export function queryRenderedFeatures(
sourceCache: SourceCache,
styleLayers: {
[_: string]: StyleLayer;
},
styleLayers: Record<string, StyleLayer>,
queryGeometry: QueryGeometry,
filter: FilterSpecification,
layers: string[],
Expand Down
4 changes: 1 addition & 3 deletions src/source/tile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,9 +451,7 @@ class Tile {
// Queries non-symbol features rendered for this tile.
// Symbol features are queried globally
queryRenderedFeatures(
layers: {
[_: string]: StyleLayer;
},
layers: Record<string, StyleLayer>,
sourceFeatureState: SourceFeatureState,
tileResult: TilespaceQueryGeometry,
filter: FilterSpecification,
Expand Down
2 changes: 1 addition & 1 deletion src/style-spec/reference/v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@
"featuresets": {
"experimental": true,
"type": "featuresets",
"doc": "Defines sets of features for querying, interaction, and feature state manipulation.",
"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.",
"example": {
"poi": {
"selectors": [
Expand Down
6 changes: 2 additions & 4 deletions src/style/query_geometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,13 @@ export class QueryGeometry {
let aboveHorizon;

if (geometry instanceof Point || typeof geometry[0] === 'number') {
const pt = Point.convert(geometry);
const pt = Point.convert(geometry) as Point;
screenGeometry = [pt];
// @ts-expect-error - TS2345 - Argument of type 'Point | [PointLike, PointLike]' is not assignable to parameter of type 'Point'.
aboveHorizon = transform.isPointAboveHorizon(pt);
} else {
const tl = Point.convert(geometry[0]);
const br = Point.convert(geometry[1]);
const br = Point.convert(geometry[1]) as Point;
screenGeometry = [tl, br];
// @ts-expect-error - TS2345 - Argument of type 'number | Point' is not assignable to parameter of type 'Point'.
aboveHorizon = polygonizeBounds(tl, br).every((p) => transform.isPointAboveHorizon(p));
}

Expand Down
Loading

0 comments on commit b86a1ab

Please sign in to comment.