Skip to content

Commit

Permalink
s̶y̶m̶b̶o̶l̶-̶c̶l̶i̶p̶ dynamic-filtering with pitch and `distance-f…
Browse files Browse the repository at this point in the history
…rom-camera` expressions (#10795)

* Setup plumbing for pitch and distance-from-camera expressions

* Add symbol-clip with support for pitch and distance-from-camera expressions

* hackily working pitch expression

* distance clipping

* Track clipped state in JointPlacement and JointOpacity, reflect clipping using notUsed state in debug buffers

* Rename distance-from-camera to distance-from-center

* symbol-clip: splitting filter expressions into dynamic and static parts (#10923)

* symbol-clip: dynamic filter style spec changes (#10977)

* symbol-clip: Render tests, debug page and distance matrix rework (#11065)

* WIP dynamic filter splitting stage 1

* Add tests for isDynamicFilter

* More test cases for isDynamicFilter

* Existing static filters get passed through unmodified.

* WIP extracting static filters

* Working case -> any translation

* More tests for case -> any conversion

* Add support for match branches

* WIP: V0 of adding collapsing of dynamic expressions to true

* more test cases

* tests and some more refactoring

* remove temory inspection code from unit tests

* Fix dynamic filter detection

* Fix failing spec test

* Units tests hopefully :green: now

* Add test to cover for `null` in expressions

* Address CR comments and reduce number of temporary allocations

* remove gl-matrix as dependency of style-spec and inline a matrix multiplication function

* Fix lint issues

* Add better error messages for expression compilation failures

* Ensure location parameter is passed down

* Remove symbol-clip from the style-spec

* Remove unused property-expression type

* Move filterSpec to v8.json and replace symbol-clip with dynamic filter

* Fix unit tests

* Fix most flow errors

* finally flow is happy

* Add expression validation code

* Add api-supported tests for validation

* Fix some failing unit tests

* Ensure layer._featureFIlter is updated on main thread as well

* Ensure layerType is passed down to validation

* Fix flow and linting

* Fix unit tests

* Pass 1 of addressing CR comments

* Add basic placementTime metric

* Move to using total placement time

* Fix lint errors

* simplify placement time tracking

* Fix silent conflict in with SymbolInstanceStruct changes

* try benchmarking with filter splitting algorithm removed

* Fix versions microbenchmark page

* Revert "try benchmarking with filter splitting algorithm removed"

This reverts commit 865f354.

* Simplify by calculating distance using tile coordinates of the symbol directly

* Use new specific centerDistanceMatrix

* Sign flipping for consistency

* add new debug page with distance visualizer

* First set of pure distance based render tests

* Pitch thresholding tests

* Fix lint errors

* More tests

* Add first batch of point symbol render tests

* Increase threshold

* Increase allowed threshold for the correct tests 🤦

* remove flaky collision debug boxes instead

* Move geojson test data to be in separate files instead of inlined into styles

* Update flaky render test expectation

* Fix distanceMatrix comment

Co-authored-by: Aidan H <aidan.hendrickson@mapbox.com>

* Switch to linear scale and update line placement tests

* Update point placement tests

* Update test expectations

* Fix lint error

* Remove flaky collision boxes

Co-authored-by: Aidan H <aidan.hendrickson@mapbox.com>

* Remove frametime logging change

* Ensure that feature deserialization happens only when needed

* Fix matrixKey naming leftover from copying fogMatrix code

Co-authored-by: Karim Naaji <karim.naaji@gmail.com>

* Rename matrices as per CR comments

* Default layerType inside filter validation function.

* add `VectorTileFeature` deserialization cache

* Switch to matrix-free distance calculation

* Add cache in Transform

* Precompute bearing vector

* prescale by windowScaleFactor

* Inline `getSymbolFeature`

* Lazy filter compilation

* Move distance matrix calculation out to the debug page

Co-authored-by: Aidan H <aidan.hendrickson@mapbox.com>
Co-authored-by: Karim Naaji <karim.naaji@gmail.com>
  • Loading branch information
3 people authored Oct 7, 2021
1 parent e8ca715 commit cb4778f
Show file tree
Hide file tree
Showing 90 changed files with 8,678 additions and 101 deletions.
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
142 changes: 142 additions & 0 deletions debug/dynamic-filter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<!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 calcMercatorDistanceMatrix() {
const center = map.transform.point;

const m = new Float64Array(16);
const worldSize = map.transform.worldSize;
const windowScaleFactor = 1 / map.transform.height;
glMatrix.mat4.fromScaling(m, [windowScaleFactor, -windowScaleFactor, windowScaleFactor]);
glMatrix.mat4.rotateZ(m, m, map.transform.angle);
glMatrix.mat4.translate(m, m, [-center.x, -center.y, 0]);

return glMatrix.mat4.scale([], m, [worldSize, worldSize, 1]);
}

function generateDistanceScales() {
const center = map.getCenter();
const bearing = map.getBearing();
const numSteps = 10;
const step = 0.25;

const matrix = glMatrix.mat4.invert([], calcMercatorDistanceMatrix());
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 = calcMercatorDistanceMatrix();
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
37 changes: 37 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 @@ -107,6 +108,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 +138,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 +1265,39 @@ class Transform {
return posMatrix;
}

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

//Calculate the offset of the tile
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;

// Calculate the bearing vector by rotating unit vector [0, -1] clockwise
const angle = this.angle;
const bX = Math.sin(-angle);
const bY = -Math.cos(-angle);

const cX = (center.x - tX) * windowScaleFactor;
const cY = (center.y - tY) * windowScaleFactor;
cache[distanceDataKey] = {
bearing: [bX, bY],
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 +1636,7 @@ class Transform {
this.pixelMatrix = mat4.multiply(new Float64Array(16), this.labelPlaneMatrix, this.projMatrix);

this._calcFogMatrices();
this._distanceTileDataCache = {};

// inverse matrix for conversion from screen coordinates to location
m = mat4.invert(new Float64Array(16), this.pixelMatrix);
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
30 changes: 30 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,29 @@ 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 {x, y} = this.featureTileCoord;

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

// The bearing vector `b` (left handed)
const bX = this.featureDistanceData.bearing[0];
const bY = this.featureDistanceData.bearing[1];

// 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

0 comments on commit cb4778f

Please sign in to comment.