Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

s̶y̶m̶b̶o̶l̶-̶c̶l̶i̶p̶ dynamic-filtering with pitch and distance-from-camera expressions #10795

Merged
merged 25 commits into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5411f94
Setup plumbing for pitch and distance-from-camera expressions
May 28, 2021
f3fe926
Add symbol-clip with support for pitch and distance-from-camera expre…
Jun 1, 2021
cd5138a
hackily working pitch expression
Jun 7, 2021
89624ba
distance clipping
Jun 7, 2021
9972ba4
Track clipped state in JointPlacement and JointOpacity, reflect clipp…
Jun 8, 2021
e471ae9
Merge branch 'main' of github.com:mapbox/mapbox-gl-js into symbol-clip
Jul 22, 2021
0f9e5b3
Rename distance-from-camera to distance-from-center
Jul 23, 2021
93c2ca2
symbol-clip: splitting filter expressions into dynamic and static par…
Sep 15, 2021
0d3b8d5
symbol-clip: dynamic filter style spec changes (#10977)
Sep 29, 2021
878c6f1
Merge branch 'main' of github.com:mapbox/mapbox-gl-js into symbol-clip
Sep 29, 2021
7fe14b8
symbol-clip: Render tests, debug page and distance matrix rework (#11…
Oct 1, 2021
6da475d
Merge branch 'main' of github.com:mapbox/mapbox-gl-js into symbol-clip
Oct 4, 2021
be48d41
Remove frametime logging change
Oct 4, 2021
3993c38
Ensure that feature deserialization happens only when needed
Oct 4, 2021
7976dc9
Fix matrixKey naming leftover from copying fogMatrix code
Oct 4, 2021
6dd2b3b
Rename matrices as per CR comments
Oct 4, 2021
2d53efd
Default layerType inside filter validation function.
Oct 4, 2021
72fe604
add `VectorTileFeature` deserialization cache
Oct 4, 2021
9fe7898
Switch to matrix-free distance calculation
Oct 5, 2021
a90536b
Add cache in Transform
Oct 5, 2021
1f27501
Precompute bearing vector
Oct 5, 2021
9745c54
prescale by windowScaleFactor
Oct 5, 2021
2276f6d
Inline `getSymbolFeature`
Oct 5, 2021
441d53a
Lazy filter compilation
Oct 6, 2021
8e718f6
Move distance matrix calculation out to the debug page
Oct 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions bench/versions/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@
const params = new URLSearchParams(location.search.slice(1));
Promise.resolve(params.has('compare') ?
params.getAll('compare').filter(Boolean) :
fetch('https://api.github.com/repos/mapbox/mapbox-gl-js/releases/latest')
fetch('https://api.github.com/repos/mapbox/mapbox-gl-js/releases')
.then(response => response.json())
.then(pkg => [pkg['tag_name'], 'main']))
.then(releases => {
for (const release of releases) {
if (!release.prerelease && !release['tag_name'].includes('style-spec')) {
return [release['tag_name'], 'main'];
}
}
return ['main'];
}))
.then(versions => {
return versions
.map(v => `https://s3.amazonaws.com/mapbox-gl-js/${v}/benchmarks.js`)
Expand Down
129 changes: 129 additions & 0 deletions debug/dynamic-filter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html>
<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">
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gl-matrix@3.3.0/gl-matrix-min.js"></script>
<link rel='stylesheet' href='../dist/mapbox-gl.css' />
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
#tooltip {
position: absolute;
left: 0px;
top: 0px;
background-color: white;
z-index: 5;
}
</style>
</head>

<body>
<div id='map'>
<div id='tooltip'></div>
</div>

<script src='../dist/mapbox-gl-dev.js'></script>
<script src='../debug/access_token_generated.js'></script>
<script>

/*global glMatrix, turf*/

var map = window.map = new mapboxgl.Map({
container: 'map',
zoom: 10.852,
center: [-120.30344797631889, 38.11726797649675],
style: 'mapbox://styles/mapbox/streets-v11',
// hash: true
});

function generateDistanceScales() {
karimnaaji marked this conversation as resolved.
Show resolved Hide resolved
const center = map.getCenter();
const bearing = map.getBearing();
const numSteps = 10;
const step = 0.25;

const matrix = glMatrix.mat4.invert([], map.transform.mercatorCenterDistanceMatrix);
const lines = [];
for (let i = -numSteps; i <= numSteps; i++) {
const distance = step * i;
const v0 = [-2, distance, 0];
const v1 = [0, distance, 0];
const v2 = [2, distance, 0];
const p0 = new mapboxgl.MercatorCoordinate(...glMatrix.vec3.transformMat4([], v0, matrix)).toLngLat();
const p1 = new mapboxgl.MercatorCoordinate(...glMatrix.vec3.transformMat4([], v1, matrix)).toLngLat();
const p2 = new mapboxgl.MercatorCoordinate(...glMatrix.vec3.transformMat4([], v2, matrix)).toLngLat();
const line = turf.lineString([[p0.lng, p0.lat], [p1.lng, p1.lat], [p2.lng, p2.lat]], {distance: `${distance.toFixed(2)}`});
lines.push(line);
}

return turf.featureCollection(lines);
}

const tooltip = document.getElementById('tooltip');
map.on('mousemove', (e) => {
const loc = map.unproject(e.point);
const m = map.transform.mercatorCenterDistanceMatrix;
const {x, y, z} = mapboxgl.MercatorCoordinate.fromLngLat(loc);
const v = glMatrix.vec3.transformMat4([], [x, y, z], m);

const dist = v[1];
tooltip.innerText = dist.toFixed(2);
tooltip.style.transform = `translate(${e.point.x + 10}px,${e.point.y + 10}px)`;
});

map.once('load', () => {
map.setFilter('building-number-label',
["case",
["<", ["pitch"], 60], true,
["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 0]], true,
false
]
);

map.addSource('rings', {
type: 'geojson',
data: {
"type": "FeatureCollection",
"features": []
}
});

map.addLayer({
type: 'line',
id: 'rings-layer',
source: 'rings',
paint: {
"line-width": 10
}
});

map.addLayer({
type: 'symbol',
id: 'rings-labels',
source: 'rings',
layout: {
"symbol-placement": 'line',
"text-field": ["get", "distance"],
"text-pitch-alignment": "viewport",
"text-allow-overlap": true
},
paint: {
"text-color": 'red',
"text-halo-color": 'white',
"text-halo-width": 2
}
});

map.on('idle', () => {
const scale = generateDistanceScales();
map.getSource('rings').setData(scale);
});

});

</script>
</body>
</html>
2 changes: 1 addition & 1 deletion debug/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
container: 'map',
zoom: 12.5,
center: [-122.4194, 37.7749],
style: 'mapbox://styles/mapbox/streets-v10',
style: 'mapbox://styles/mapbox/streets-v11',
hash: true
});

Expand Down
22 changes: 22 additions & 0 deletions src/data/feature_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class FeatureIndex {
bucketLayerIDs: Array<Array<string>>;

vtLayers: {[_: string]: VectorTileLayer};
vtFeatures: {[_: string]: VectorTileFeature[]};
sourceLayerCoder: DictionaryCoder;

constructor(tileID: OverscaledTileID, promoteId?: ?PromoteIdSpecification) {
Expand Down Expand Up @@ -102,6 +103,10 @@ class FeatureIndex {
if (!this.vtLayers) {
this.vtLayers = new vt.VectorTile(new Protobuf(this.rawTileData)).layers;
this.sourceLayerCoder = new DictionaryCoder(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']);
this.vtFeatures = {};
for (const layer in this.vtLayers) {
this.vtFeatures[layer] = [];
}
}
return this.vtLayers;
}
Expand Down Expand Up @@ -262,6 +267,23 @@ class FeatureIndex {
return result;
}

loadFeature(featureIndexData: FeatureIndices): VectorTileFeature {
const {featureIndex, sourceLayerIndex} = featureIndexData;

this.loadVTLayers();
const sourceLayerName = this.sourceLayerCoder.decode(sourceLayerIndex);

const featureCache = this.vtFeatures[sourceLayerName];
if (featureCache[featureIndex]) {
return featureCache[featureIndex];
}
const sourceLayer = this.vtLayers[sourceLayerName];
const feature = sourceLayer.feature(featureIndex);
featureCache[featureIndex] = feature;

return feature;
}

hasLayer(id: string) {
for (const layerIDs of this.bucketLayerIDs) {
for (const layerID of layerIDs) {
Expand Down
53 changes: 53 additions & 0 deletions src/geo/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import assert from 'assert';
import {UnwrappedTileID, OverscaledTileID, CanonicalTileID} from '../source/tile_id.js';
import type {Elevation} from '../terrain/elevation.js';
import type {PaddingOptions} from './edge_insets.js';
import type {FeatureDistanceData} from '../style-spec/feature_filter/index.js';

const NUM_WORLD_COPIES = 3;
const DEFAULT_MIN_ZOOM = 0;
Expand Down Expand Up @@ -76,6 +77,13 @@ class Transform {
pixelMatrix: Float64Array;
pixelMatrixInverse: Float64Array;

// From world coordinates to, relative-to-center coordinates.
// Relative-to-center coordinates are centered at the current map center, and rotated by the map bearing.
// +x basis goes towards the right, +y basis point away from the camera, +z basis points up from the map.
worldToCenterDistanceMatrix: Float64Array;
karimnaaji marked this conversation as resolved.
Show resolved Hide resolved
// From Mercator coordinates to relative to center coordinates
mercatorCenterDistanceMatrix: Float64Array;
karimnaaji marked this conversation as resolved.
Show resolved Hide resolved

worldToFogMatrix: Float64Array;
skyboxMatrix: Float32Array;

Expand Down Expand Up @@ -107,6 +115,7 @@ class Transform {
_projMatrixCache: {[_: number]: Float32Array};
_alignedProjMatrixCache: {[_: number]: Float32Array};
_fogTileMatrixCache: {[_: number]: Float32Array};
_distanceTileDataCache: {[_: number]: FeatureDistanceData};
_camera: FreeCamera;
_centerAltitude: number;
_horizonShift: number;
Expand Down Expand Up @@ -136,6 +145,7 @@ class Transform {
this._projMatrixCache = {};
this._alignedProjMatrixCache = {};
this._fogTileMatrixCache = {};
this._distanceTileDataCache = {};
this._camera = new FreeCamera();
this._centerAltitude = 0;
this._averageElevation = 0;
Expand Down Expand Up @@ -1262,6 +1272,34 @@ class Transform {
return posMatrix;
}

calculateDistanceTileData(unwrappedTileID: UnwrappedTileID): FeatureDistanceData {
const distanceDataKey = unwrappedTileID.key;
const cache = this._distanceTileDataCache;
if (cache[distanceDataKey]) {
return cache[distanceDataKey];
}

const canonical = unwrappedTileID.canonical;
const windowScaleFactor = 1 / this.height;
const scale = this.cameraWorldSize / this.zoomScale(canonical.z);
const unwrappedX = canonical.x + Math.pow(2, canonical.z) * unwrappedTileID.wrap;
const tX = unwrappedX * scale;
const tY = canonical.y * scale;

const center = this.point;

const cX = center.x - tX;
const cY = center.y - tY;
cache[distanceDataKey] = {
angle: this.angle,
center: [cX, cY],
scale: scale / EXTENT,
windowScaleFactor
};

return cache[distanceDataKey];
}

/**
* Calculate the fogTileMatrix that, given a tile coordinate, can be used to
* calculate its position relative to the camera in units of pixels divided
Expand Down Expand Up @@ -1600,6 +1638,7 @@ class Transform {
this.pixelMatrix = mat4.multiply(new Float64Array(16), this.labelPlaneMatrix, this.projMatrix);

this._calcFogMatrices();
this._calcDistanceMatrices();
karimnaaji marked this conversation as resolved.
Show resolved Hide resolved

// inverse matrix for conversion from screen coordinates to location
m = mat4.invert(new Float64Array(16), this.pixelMatrix);
Expand Down Expand Up @@ -1639,6 +1678,20 @@ class Transform {
this.worldToFogMatrix = this._camera.getWorldToCameraPosition(cameraWorldSize, cameraPixelsPerMeter, windowScaleFactor);
}

_calcDistanceMatrices() {
karimnaaji marked this conversation as resolved.
Show resolved Hide resolved
this._distanceTileDataCache = {};
const center = this.point;

const m = new Float64Array(16);
const windowScaleFactor = 1 / this.height;
mat4.fromScaling(m, [windowScaleFactor, -windowScaleFactor, windowScaleFactor]);
karimnaaji marked this conversation as resolved.
Show resolved Hide resolved
mat4.rotateZ(m, m, this.angle);
mat4.translate(m, m, [-center.x, -center.y, 0]);

this.worldToCenterDistanceMatrix = m;
this.mercatorCenterDistanceMatrix = mat4.scale([], m, [this.worldSize, this.worldSize, 1]);
karimnaaji marked this conversation as resolved.
Show resolved Hide resolved
}

_updateCameraState() {
if (!this.height) return;

Expand Down
10 changes: 10 additions & 0 deletions src/style-spec/expression/definitions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,16 @@ CompoundExpression.register(expressions, {
[],
(ctx) => ctx.globals.zoom
],
'pitch': [
NumberType,
[],
(ctx) => ctx.globals.pitch || 0
],
'distance-from-center': [
NumberType,
[],
(ctx) => ctx.distanceFromCenter()
],
'heatmap-density': [
NumberType,
[],
Expand Down
34 changes: 34 additions & 0 deletions src/style-spec/expression/evaluation_context.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// @flow

import {Color} from './values.js';

import type Point from '@mapbox/point-geometry';
import type {FormattedSection} from './types/formatted.js';
import type {GlobalProperties, Feature, FeatureState} from './index.js';
import type {CanonicalTileID} from '../../source/tile_id.js';
import type {FeatureDistanceData} from '../feature_filter/index.js';

const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon'];

Expand All @@ -14,6 +17,8 @@ class EvaluationContext {
formattedSection: ?FormattedSection;
availableImages: ?Array<string>;
canonical: ?CanonicalTileID;
featureTileCoord: ?Point;
featureDistanceData: ?FeatureDistanceData;

_parseColorCache: {[_: string]: ?Color};

Expand All @@ -25,6 +30,8 @@ class EvaluationContext {
this._parseColorCache = {};
this.availableImages = null;
this.canonical = null;
this.featureTileCoord = null;
this.featureDistanceData = null;
}

id() {
Expand All @@ -47,6 +54,33 @@ class EvaluationContext {
return this.feature && this.feature.properties || {};
}

distanceFromCenter() {
if (this.featureTileCoord && this.featureDistanceData) {

const c = this.featureDistanceData.center;
const scale = this.featureDistanceData.scale;
const angle = this.featureDistanceData.angle;
const {x, y} = this.featureTileCoord;

// Calculate the distance vector `d`
const dX = (x * scale - c[0]) * this.featureDistanceData.windowScaleFactor;
const dY = (y * scale - c[1]) * this.featureDistanceData.windowScaleFactor;

// Calculate the bearing vector `v` by rotating unit vector clockwise
const b = [0, -1];
const cos = Math.cos(-angle);
const sin = Math.sin(-angle);
const bX = cos * b[0] - sin * b[1];
const bY = sin * b[0] + cos * b[1];
karimnaaji marked this conversation as resolved.
Show resolved Hide resolved

// Distance is calculated as `dot(d, v)`
const dist = (bX * dX + bY * dY);
return dist;
}

return 0;
}

parseColor(input: string): ?Color {
let cached = this._parseColorCache[input];
if (!cached) {
Expand Down
Loading