UNPKG

8.78 kBPlain TextView Raw
1import StyleLayer from '../style_layer';
2
3import FillExtrusionBucket from '../../data/bucket/fill_extrusion_bucket';
4import {polygonIntersectsPolygon, polygonIntersectsMultiPolygon} from '../../util/intersection_tests';
5import {translateDistance, translate} from '../query_utils';
6import properties, {FillExtrusionPaintPropsPossiblyEvaluated} from './fill_extrusion_style_layer_properties.g';
7import {Transitionable, Transitioning, PossiblyEvaluated} from '../properties';
8import {mat4, vec4} from 'gl-matrix';
9import Point from '@mapbox/point-geometry';
10import type {FeatureState} from '../../style-spec/expression';
11import type {BucketParameters} from '../../data/bucket';
12import type {FillExtrusionPaintProps} from './fill_extrusion_style_layer_properties.g';
13import type Transform from '../../geo/transform';
14import type {LayerSpecification} from '../../style-spec/types.g';
15import type {VectorTileFeature} from '@mapbox/vector-tile';
16
17export class Point3D extends Point {
18 z: number;
19}
20
21class FillExtrusionStyleLayer extends StyleLayer {
22 _transitionablePaint: Transitionable<FillExtrusionPaintProps>;
23 _transitioningPaint: Transitioning<FillExtrusionPaintProps>;
24 paint: PossiblyEvaluated<FillExtrusionPaintProps, FillExtrusionPaintPropsPossiblyEvaluated>;
25
26 constructor(layer: LayerSpecification) {
27 super(layer, properties);
28 }
29
30 createBucket(parameters: BucketParameters<FillExtrusionStyleLayer>) {
31 return new FillExtrusionBucket(parameters);
32 }
33
34 queryRadius(): number {
35 return translateDistance(this.paint.get('fill-extrusion-translate'));
36 }
37
38 is3D(): boolean {
39 return true;
40 }
41
42 queryIntersectsFeature(
43 queryGeometry: Array<Point>,
44 feature: VectorTileFeature,
45 featureState: FeatureState,
46 geometry: Array<Array<Point>>,
47 zoom: number,
48 transform: Transform,
49 pixelsToTileUnits: number,
50 pixelPosMatrix: mat4
51 ): boolean | number {
52
53 const translatedPolygon = translate(queryGeometry,
54 this.paint.get('fill-extrusion-translate'),
55 this.paint.get('fill-extrusion-translate-anchor'),
56 transform.angle, pixelsToTileUnits);
57
58 const height = this.paint.get('fill-extrusion-height').evaluate(feature, featureState);
59 const base = this.paint.get('fill-extrusion-base').evaluate(feature, featureState);
60
61 const projectedQueryGeometry = projectQueryGeometry(translatedPolygon, pixelPosMatrix, transform, 0);
62
63 const projected = projectExtrusion(geometry, base, height, pixelPosMatrix);
64 const projectedBase = projected[0];
65 const projectedTop = projected[1];
66 return checkIntersection(projectedBase, projectedTop, projectedQueryGeometry);
67 }
68}
69
70function dot(a, b) {
71 return a.x * b.x + a.y * b.y;
72}
73
74export function getIntersectionDistance(projectedQueryGeometry: Array<Point3D>, projectedFace: Array<Point3D>) {
75
76 if (projectedQueryGeometry.length === 1) {
77 // For point queries calculate the z at which the point intersects the face
78 // using barycentric coordinates.
79
80 // Find the barycentric coordinates of the projected point within the first
81 // triangle of the face, using only the xy plane. It doesn't matter if the
82 // point is outside the first triangle because all the triangles in the face
83 // are in the same plane.
84 //
85 // Check whether points are coincident and use other points if they are.
86 let i = 0;
87 const a = projectedFace[i++];
88 let b;
89 while (!b || a.equals(b)) {
90 b = projectedFace[i++];
91 if (!b) return Infinity;
92 }
93
94 // Loop until point `c` is not colinear with points `a` and `b`.
95 for (; i < projectedFace.length; i++) {
96 const c = projectedFace[i];
97
98 const p = projectedQueryGeometry[0];
99
100 const ab = b.sub(a);
101 const ac = c.sub(a);
102 const ap = p.sub(a);
103
104 const dotABAB = dot(ab, ab);
105 const dotABAC = dot(ab, ac);
106 const dotACAC = dot(ac, ac);
107 const dotAPAB = dot(ap, ab);
108 const dotAPAC = dot(ap, ac);
109 const denom = dotABAB * dotACAC - dotABAC * dotABAC;
110
111 const v = (dotACAC * dotAPAB - dotABAC * dotAPAC) / denom;
112 const w = (dotABAB * dotAPAC - dotABAC * dotAPAB) / denom;
113 const u = 1 - v - w;
114
115 // Use the barycentric weighting along with the original triangle z coordinates to get the point of intersection.
116 const distance = a.z * u + b.z * v + c.z * w;
117
118 if (isFinite(distance)) return distance;
119 }
120
121 return Infinity;
122
123 } else {
124 // The counts as closest is less clear when the query is a box. This
125 // returns the distance to the nearest point on the face, whether it is
126 // within the query or not. It could be more correct to return the
127 // distance to the closest point within the query box but this would be
128 // more complicated and expensive to calculate with little benefit.
129 let closestDistance = Infinity;
130 for (const p of projectedFace) {
131 closestDistance = Math.min(closestDistance, p.z);
132 }
133 return closestDistance;
134 }
135}
136
137function checkIntersection(projectedBase: Array<Array<Point3D>>, projectedTop: Array<Array<Point3D>>, projectedQueryGeometry: Array<Point3D>) {
138 let closestDistance = Infinity;
139
140 if (polygonIntersectsMultiPolygon(projectedQueryGeometry, projectedTop)) {
141 closestDistance = getIntersectionDistance(projectedQueryGeometry, projectedTop[0]);
142 }
143
144 for (let r = 0; r < projectedTop.length; r++) {
145 const ringTop = projectedTop[r];
146 const ringBase = projectedBase[r];
147 for (let p = 0; p < ringTop.length - 1; p++) {
148 const topA = ringTop[p];
149 const topB = ringTop[p + 1];
150 const baseA = ringBase[p];
151 const baseB = ringBase[p + 1];
152 const face = [topA, topB, baseB, baseA, topA];
153 if (polygonIntersectsPolygon(projectedQueryGeometry, face)) {
154 closestDistance = Math.min(closestDistance, getIntersectionDistance(projectedQueryGeometry, face));
155 }
156 }
157 }
158
159 return closestDistance === Infinity ? false : closestDistance;
160}
161
162/*
163 * Project the geometry using matrix `m`. This is essentially doing
164 * `vec4.transformMat4([], [p.x, p.y, z, 1], m)` but the multiplication
165 * is inlined so that parts of the projection that are the same across
166 * different points can only be done once. This produced a measurable
167 * performance improvement.
168 */
169function projectExtrusion(geometry: Array<Array<Point>>, zBase: number, zTop: number, m: mat4): [Array<Array<Point3D>>, Array<Array<Point3D>>] {
170 const projectedBase = [] as Array<Array<Point3D>>;
171 const projectedTop = [] as Array<Array<Point3D>>;
172 const baseXZ = m[8] * zBase;
173 const baseYZ = m[9] * zBase;
174 const baseZZ = m[10] * zBase;
175 const baseWZ = m[11] * zBase;
176 const topXZ = m[8] * zTop;
177 const topYZ = m[9] * zTop;
178 const topZZ = m[10] * zTop;
179 const topWZ = m[11] * zTop;
180
181 for (const r of geometry) {
182 const ringBase = [] as Array<Point3D>;
183 const ringTop = [] as Array<Point3D>;
184 for (const p of r) {
185 const x = p.x;
186 const y = p.y;
187
188 const sX = m[0] * x + m[4] * y + m[12];
189 const sY = m[1] * x + m[5] * y + m[13];
190 const sZ = m[2] * x + m[6] * y + m[14];
191 const sW = m[3] * x + m[7] * y + m[15];
192
193 const baseX = sX + baseXZ;
194 const baseY = sY + baseYZ;
195 const baseZ = sZ + baseZZ;
196 const baseW = sW + baseWZ;
197
198 const topX = sX + topXZ;
199 const topY = sY + topYZ;
200 const topZ = sZ + topZZ;
201 const topW = sW + topWZ;
202
203 const b = new Point(baseX / baseW, baseY / baseW) as Point3D;
204 b.z = baseZ / baseW;
205 ringBase.push(b);
206
207 const t = new Point(topX / topW, topY / topW) as Point3D;
208 t.z = topZ / topW;
209 ringTop.push(t);
210 }
211 projectedBase.push(ringBase);
212 projectedTop.push(ringTop);
213 }
214 return [projectedBase, projectedTop];
215}
216
217function projectQueryGeometry(queryGeometry: Array<Point>, pixelPosMatrix: mat4, transform: Transform, z: number) {
218 const projectedQueryGeometry = [];
219 for (const p of queryGeometry) {
220 const v = [p.x, p.y, z, 1] as vec4;
221 vec4.transformMat4(v, v, pixelPosMatrix);
222 projectedQueryGeometry.push(new Point(v[0] / v[3], v[1] / v[3]));
223 }
224 return projectedQueryGeometry;
225}
226
227export default FillExtrusionStyleLayer;