maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
228 lines (189 loc) • 8.78 kB
text/typescript
import StyleLayer from '../style_layer';
import FillExtrusionBucket from '../../data/bucket/fill_extrusion_bucket';
import {polygonIntersectsPolygon, polygonIntersectsMultiPolygon} from '../../util/intersection_tests';
import {translateDistance, translate} from '../query_utils';
import properties, {FillExtrusionPaintPropsPossiblyEvaluated} from './fill_extrusion_style_layer_properties.g';
import {Transitionable, Transitioning, PossiblyEvaluated} from '../properties';
import {mat4, vec4} from 'gl-matrix';
import Point from '@mapbox/point-geometry';
import type {FeatureState} from '../../style-spec/expression';
import type {BucketParameters} from '../../data/bucket';
import type {FillExtrusionPaintProps} from './fill_extrusion_style_layer_properties.g';
import type Transform from '../../geo/transform';
import type {LayerSpecification} from '../../style-spec/types.g';
import type {VectorTileFeature} from '@mapbox/vector-tile';
export class Point3D extends Point {
z: number;
}
class FillExtrusionStyleLayer extends StyleLayer {
_transitionablePaint: Transitionable<FillExtrusionPaintProps>;
_transitioningPaint: Transitioning<FillExtrusionPaintProps>;
paint: PossiblyEvaluated<FillExtrusionPaintProps, FillExtrusionPaintPropsPossiblyEvaluated>;
constructor(layer: LayerSpecification) {
super(layer, properties);
}
createBucket(parameters: BucketParameters<FillExtrusionStyleLayer>) {
return new FillExtrusionBucket(parameters);
}
queryRadius(): number {
return translateDistance(this.paint.get('fill-extrusion-translate'));
}
is3D(): boolean {
return true;
}
queryIntersectsFeature(
queryGeometry: Array<Point>,
feature: VectorTileFeature,
featureState: FeatureState,
geometry: Array<Array<Point>>,
zoom: number,
transform: Transform,
pixelsToTileUnits: number,
pixelPosMatrix: mat4
): boolean | number {
const translatedPolygon = translate(queryGeometry,
this.paint.get('fill-extrusion-translate'),
this.paint.get('fill-extrusion-translate-anchor'),
transform.angle, pixelsToTileUnits);
const height = this.paint.get('fill-extrusion-height').evaluate(feature, featureState);
const base = this.paint.get('fill-extrusion-base').evaluate(feature, featureState);
const projectedQueryGeometry = projectQueryGeometry(translatedPolygon, pixelPosMatrix, transform, 0);
const projected = projectExtrusion(geometry, base, height, pixelPosMatrix);
const projectedBase = projected[0];
const projectedTop = projected[1];
return checkIntersection(projectedBase, projectedTop, projectedQueryGeometry);
}
}
function dot(a, b) {
return a.x * b.x + a.y * b.y;
}
export function getIntersectionDistance(projectedQueryGeometry: Array<Point3D>, projectedFace: Array<Point3D>) {
if (projectedQueryGeometry.length === 1) {
// For point queries calculate the z at which the point intersects the face
// using barycentric coordinates.
// Find the barycentric coordinates of the projected point within the first
// triangle of the face, using only the xy plane. It doesn't matter if the
// point is outside the first triangle because all the triangles in the face
// are in the same plane.
//
// Check whether points are coincident and use other points if they are.
let i = 0;
const a = projectedFace[i++];
let b;
while (!b || a.equals(b)) {
b = projectedFace[i++];
if (!b) return Infinity;
}
// Loop until point `c` is not colinear with points `a` and `b`.
for (; i < projectedFace.length; i++) {
const c = projectedFace[i];
const p = projectedQueryGeometry[0];
const ab = b.sub(a);
const ac = c.sub(a);
const ap = p.sub(a);
const dotABAB = dot(ab, ab);
const dotABAC = dot(ab, ac);
const dotACAC = dot(ac, ac);
const dotAPAB = dot(ap, ab);
const dotAPAC = dot(ap, ac);
const denom = dotABAB * dotACAC - dotABAC * dotABAC;
const v = (dotACAC * dotAPAB - dotABAC * dotAPAC) / denom;
const w = (dotABAB * dotAPAC - dotABAC * dotAPAB) / denom;
const u = 1 - v - w;
// Use the barycentric weighting along with the original triangle z coordinates to get the point of intersection.
const distance = a.z * u + b.z * v + c.z * w;
if (isFinite(distance)) return distance;
}
return Infinity;
} else {
// The counts as closest is less clear when the query is a box. This
// returns the distance to the nearest point on the face, whether it is
// within the query or not. It could be more correct to return the
// distance to the closest point within the query box but this would be
// more complicated and expensive to calculate with little benefit.
let closestDistance = Infinity;
for (const p of projectedFace) {
closestDistance = Math.min(closestDistance, p.z);
}
return closestDistance;
}
}
function checkIntersection(projectedBase: Array<Array<Point3D>>, projectedTop: Array<Array<Point3D>>, projectedQueryGeometry: Array<Point3D>) {
let closestDistance = Infinity;
if (polygonIntersectsMultiPolygon(projectedQueryGeometry, projectedTop)) {
closestDistance = getIntersectionDistance(projectedQueryGeometry, projectedTop[0]);
}
for (let r = 0; r < projectedTop.length; r++) {
const ringTop = projectedTop[r];
const ringBase = projectedBase[r];
for (let p = 0; p < ringTop.length - 1; p++) {
const topA = ringTop[p];
const topB = ringTop[p + 1];
const baseA = ringBase[p];
const baseB = ringBase[p + 1];
const face = [topA, topB, baseB, baseA, topA];
if (polygonIntersectsPolygon(projectedQueryGeometry, face)) {
closestDistance = Math.min(closestDistance, getIntersectionDistance(projectedQueryGeometry, face));
}
}
}
return closestDistance === Infinity ? false : closestDistance;
}
/*
* Project the geometry using matrix `m`. This is essentially doing
* `vec4.transformMat4([], [p.x, p.y, z, 1], m)` but the multiplication
* is inlined so that parts of the projection that are the same across
* different points can only be done once. This produced a measurable
* performance improvement.
*/
function projectExtrusion(geometry: Array<Array<Point>>, zBase: number, zTop: number, m: mat4): [Array<Array<Point3D>>, Array<Array<Point3D>>] {
const projectedBase = [] as Array<Array<Point3D>>;
const projectedTop = [] as Array<Array<Point3D>>;
const baseXZ = m[8] * zBase;
const baseYZ = m[9] * zBase;
const baseZZ = m[10] * zBase;
const baseWZ = m[11] * zBase;
const topXZ = m[8] * zTop;
const topYZ = m[9] * zTop;
const topZZ = m[10] * zTop;
const topWZ = m[11] * zTop;
for (const r of geometry) {
const ringBase = [] as Array<Point3D>;
const ringTop = [] as Array<Point3D>;
for (const p of r) {
const x = p.x;
const y = p.y;
const sX = m[0] * x + m[4] * y + m[12];
const sY = m[1] * x + m[5] * y + m[13];
const sZ = m[2] * x + m[6] * y + m[14];
const sW = m[3] * x + m[7] * y + m[15];
const baseX = sX + baseXZ;
const baseY = sY + baseYZ;
const baseZ = sZ + baseZZ;
const baseW = sW + baseWZ;
const topX = sX + topXZ;
const topY = sY + topYZ;
const topZ = sZ + topZZ;
const topW = sW + topWZ;
const b = new Point(baseX / baseW, baseY / baseW) as Point3D;
b.z = baseZ / baseW;
ringBase.push(b);
const t = new Point(topX / topW, topY / topW) as Point3D;
t.z = topZ / topW;
ringTop.push(t);
}
projectedBase.push(ringBase);
projectedTop.push(ringTop);
}
return [projectedBase, projectedTop];
}
function projectQueryGeometry(queryGeometry: Array<Point>, pixelPosMatrix: mat4, transform: Transform, z: number) {
const projectedQueryGeometry = [];
for (const p of queryGeometry) {
const v = [p.x, p.y, z, 1] as vec4;
vec4.transformMat4(v, v, pixelPosMatrix);
projectedQueryGeometry.push(new Point(v[0] / v[3], v[1] / v[3]));
}
return projectedQueryGeometry;
}
export default FillExtrusionStyleLayer;