Skip to content

Commit 96c4b66

Browse files
authored
Use TileCoordinates instead of LngLats for within calculation (#9428)
* Using TileCoordinates instead of LngLat for within calculation * Update tests * Adapt test data * Add render tests
1 parent 7436884 commit 96c4b66

File tree

15 files changed

+1583
-81
lines changed

15 files changed

+1583
-81
lines changed

src/style-spec/expression/definitions/within.js

+95-71
Original file line numberDiff line numberDiff line change
@@ -10,65 +10,31 @@ import type {GeoJSON, GeoJSONPolygon, GeoJSONMultiPolygon} from '@mapbox/geojson
1010
import MercatorCoordinate from '../../../geo/mercator_coordinate';
1111
import EXTENT from '../../../data/extent';
1212
import Point from '@mapbox/point-geometry';
13+
import type {CanonicalTileID} from '../../../source/tile_id';
1314

1415
type GeoJSONPolygons =| GeoJSONPolygon | GeoJSONMultiPolygon;
1516

17+
// minX, minY, maxX, maxY
1618
type BBox = [number, number, number, number];
17-
18-
function calcBBox(bbox: BBox, geom, type) {
19-
if (type === 'Point') {
20-
updateBBox(bbox, geom);
21-
} else if (type === 'MultiPoint' || type === 'LineString') {
22-
for (let i = 0; i < geom.length; ++i) {
23-
updateBBox(bbox, geom[i]);
24-
}
25-
} else if (type === 'Polygon' || type === 'MultiLineString') {
26-
for (let i = 0; i < geom.length; i++) {
27-
for (let j = 0; j < geom[i].length; j++) {
28-
updateBBox(bbox, geom[i][j]);
29-
}
30-
}
31-
} else if (type === 'MultiPolygon') {
32-
for (let i = 0; i < geom.length; i++) {
33-
for (let j = 0; j < geom[i].length; j++) {
34-
for (let k = 0; k < geom[i][j].length; k++) {
35-
updateBBox(bbox, geom[i][j][k]);
36-
}
37-
}
38-
}
39-
}
40-
}
41-
4219
function updateBBox(bbox: BBox, coord: Point) {
4320
bbox[0] = Math.min(bbox[0], coord[0]);
4421
bbox[1] = Math.min(bbox[1], coord[1]);
4522
bbox[2] = Math.max(bbox[2], coord[0]);
4623
bbox[3] = Math.max(bbox[3], coord[1]);
4724
}
4825

49-
function boxWithinBox(bbox1, bbox2) {
26+
function boxWithinBox(bbox1: BBox, bbox2: BBox) {
5027
if (bbox1[0] <= bbox2[0]) return false;
5128
if (bbox1[2] >= bbox2[2]) return false;
5229
if (bbox1[1] <= bbox2[1]) return false;
5330
if (bbox1[3] >= bbox2[3]) return false;
5431
return true;
5532
}
5633

57-
function getLngLatPoint(coord: Point, canonical) {
34+
function getTileCoordinates(p, canonical: CanonicalTileID) {
35+
const coord = MercatorCoordinate.fromLngLat({lng: p[0], lat: p[1]}, 0);
5836
const tilesAtZoom = Math.pow(2, canonical.z);
59-
const x = (coord.x / EXTENT + canonical.x) / tilesAtZoom;
60-
const y = (coord.y / EXTENT + canonical.y) / tilesAtZoom;
61-
const p = new MercatorCoordinate(x, y).toLngLat();
62-
63-
return [p.lng, p.lat];
64-
}
65-
66-
function getLngLatPoints(line, canonical) {
67-
const coords = [];
68-
for (let i = 0; i < line.length; ++i) {
69-
coords.push(getLngLatPoint(line[i], canonical));
70-
}
71-
return coords;
37+
return [Math.round(coord.x * tilesAtZoom * EXTENT), Math.round(coord.y * tilesAtZoom * EXTENT)];
7238
}
7339

7440
function onBoundary(p, p1, p2) {
@@ -97,13 +63,10 @@ function pointWithinPolygon(point, rings) {
9763
}
9864

9965
function pointWithinPolygons(point, polygons) {
100-
if (polygons.type === 'Polygon') {
101-
return pointWithinPolygon(point, polygons.coordinates);
66+
for (let i = 0; i < polygons.length; i++) {
67+
if (pointWithinPolygon(point, polygons[i])) return true;
10268
}
103-
for (let i = 0; i < polygons.coordinates.length; i++) {
104-
if (!pointWithinPolygon(point, polygons.coordinates[i])) return false;
105-
}
106-
return true;
69+
return false;
10770
}
10871

10972
function perp(v1, v2) {
@@ -168,59 +131,120 @@ function lineStringWithinPolygon(line, polygon) {
168131
}
169132

170133
function lineStringWithinPolygons(line, polygons) {
171-
if (polygons.type === 'Polygon') {
172-
return lineStringWithinPolygon(line, polygons.coordinates);
134+
for (let i = 0; i < polygons.length; i++) {
135+
if (lineStringWithinPolygon(line, polygons[i])) return true;
173136
}
174-
for (let i = 0; i < polygons.coordinates.length; i++) {
175-
if (!lineStringWithinPolygon(line, polygons.coordinates[i])) return false;
137+
return false;
138+
}
139+
140+
function getTilePolygon(coordinates, bbox, canonical) {
141+
const polygon = [];
142+
for (let i = 0; i < coordinates.length; i++) {
143+
const ring = [];
144+
for (let j = 0; j < coordinates[i].length; j++) {
145+
const coord = getTileCoordinates(coordinates[i][j], canonical);
146+
updateBBox(bbox, coord);
147+
ring.push(coord);
148+
}
149+
polygon.push(ring);
176150
}
177-
return true;
151+
return polygon;
178152
}
179153

180-
function pointsWithinPolygons(ctx: EvaluationContext, polygonGeometry: GeoJSONPolygons, polyBBox: BBox) {
154+
function getTilePolygons(coordinates, bbox, canonical) {
155+
const polygons = [];
156+
for (let i = 0; i < coordinates.length; i++) {
157+
const polygon = getTilePolygon(coordinates[i], bbox, canonical);
158+
polygons.push(polygon);
159+
}
160+
return polygons;
161+
}
162+
163+
function pointsWithinPolygons(ctx: EvaluationContext, polygonGeometry: GeoJSONPolygons) {
181164
const pointBBox = [Infinity, Infinity, -Infinity, -Infinity];
182-
const lngLatPoints = [];
165+
const polyBBox = [Infinity, Infinity, -Infinity, -Infinity];
166+
const canonical = ctx.canonicalID();
167+
const shifts = [canonical.x * EXTENT, canonical.y * EXTENT];
168+
const tilePoints = [];
169+
183170
for (const points of ctx.geometry()) {
184171
for (const point of points) {
185-
const p = getLngLatPoint(point, ctx.canonicalID());
186-
lngLatPoints.push(p);
172+
const p = [point.x + shifts[0], point.y + shifts[1]];
187173
updateBBox(pointBBox, p);
174+
tilePoints.push(p);
175+
}
176+
}
177+
178+
if (polygonGeometry.type === 'Polygon') {
179+
const tilePolygon = getTilePolygon(polygonGeometry.coordinates, polyBBox, canonical);
180+
if (!boxWithinBox(pointBBox, polyBBox)) return false;
181+
182+
for (const point of tilePoints) {
183+
if (!pointWithinPolygon(point, tilePolygon)) return false;
188184
}
189185
}
190-
if (!boxWithinBox(pointBBox, polyBBox)) return false;
191-
for (let i = 0; i < lngLatPoints.length; ++i) {
192-
if (!pointWithinPolygons(lngLatPoints[i], polygonGeometry)) return false;
186+
187+
if (polygonGeometry.type === 'MultiPolygon') {
188+
const tilePolygons = getTilePolygons(polygonGeometry.coordinates, polyBBox, canonical);
189+
if (!boxWithinBox(pointBBox, polyBBox)) return false;
190+
191+
for (const point of tilePoints) {
192+
if (!pointWithinPolygons(point, tilePolygons)) return false;
193+
}
193194
}
195+
194196
return true;
195197
}
196198

197-
function linesWithinPolygons(ctx: EvaluationContext, polygonGeometry: GeoJSONPolygons, polyBBox: BBox) {
199+
function linesWithinPolygons(ctx: EvaluationContext, polygonGeometry: GeoJSONPolygons) {
198200
const lineBBox = [Infinity, Infinity, -Infinity, -Infinity];
199-
const lineCoords = [];
201+
const polyBBox = [Infinity, Infinity, -Infinity, -Infinity];
202+
203+
const canonical = ctx.canonicalID();
204+
const shifts = [canonical.x * EXTENT, canonical.y * EXTENT];
205+
const tileLines = [];
206+
200207
for (const line of ctx.geometry()) {
201-
const lineCoord = getLngLatPoints(line, ctx.canonicalID());
202-
lineCoords.push(lineCoord);
203-
calcBBox(lineBBox, lineCoord, 'LineString');
208+
const tileLine = [];
209+
for (const point of line) {
210+
const p = [point.x + shifts[0], point.y + shifts[1]];
211+
updateBBox(lineBBox, p);
212+
tileLine.push(p);
213+
}
214+
tileLines.push(tileLine);
204215
}
205-
if (!boxWithinBox(lineBBox, polyBBox)) return false;
206-
for (let i = 0; i < lineCoords.length; ++i) {
207-
if (!lineStringWithinPolygons(lineCoords[i], polygonGeometry)) return false;
216+
217+
if (polygonGeometry.type === 'Polygon') {
218+
const tilePolygon = getTilePolygon(polygonGeometry.coordinates, polyBBox, canonical);
219+
if (!boxWithinBox(lineBBox, polyBBox)) return false;
220+
221+
for (const line of tileLines) {
222+
if (!lineStringWithinPolygon(line, tilePolygon)) return false;
223+
}
224+
}
225+
226+
if (polygonGeometry.type === 'MultiPolygon') {
227+
const tilePolygons = getTilePolygons(polygonGeometry.coordinates, polyBBox, canonical);
228+
229+
if (!boxWithinBox(lineBBox, polyBBox)) return false;
230+
231+
for (const line of tileLines) {
232+
if (!lineStringWithinPolygons(line, tilePolygons)) return false;
233+
}
208234
}
209235
return true;
236+
210237
}
211238

212239
class Within implements Expression {
213240
type: Type;
214241
geojson: GeoJSON
215242
geometries: GeoJSONPolygons;
216-
polyBBox: BBox;
217243

218244
constructor(geojson: GeoJSON, geometries: GeoJSONPolygons) {
219245
this.type = BooleanType;
220246
this.geojson = geojson;
221247
this.geometries = geometries;
222-
this.polyBBox = [Infinity, Infinity, -Infinity, -Infinity];
223-
calcBBox(this.polyBBox, this.geometries.coordinates, this.geometries.type);
224248
}
225249

226250
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext) {
@@ -250,9 +274,9 @@ class Within implements Expression {
250274
evaluate(ctx: EvaluationContext) {
251275
if (ctx.geometry() != null && ctx.canonicalID() != null) {
252276
if (ctx.geometryType() === 'Point') {
253-
return pointsWithinPolygons(ctx, this.geometries, this.polyBBox);
277+
return pointsWithinPolygons(ctx, this.geometries);
254278
} else if (ctx.geometryType() === 'LineString') {
255-
return linesWithinPolygons(ctx, this.geometries, this.polyBBox);
279+
return linesWithinPolygons(ctx, this.geometries);
256280
}
257281
}
258282
return false;

test/expression.test.js

+21-10
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ import ignores from './ignores.json';
77
import {CanonicalTileID} from '../src/source/tile_id';
88
import MercatorCoordinate from '../src/geo/mercator_coordinate';
99

10-
function convertPoint(coord, canonical, out) {
10+
function getPoint(coord, canonical) {
1111
const p = canonical.getTilePoint(MercatorCoordinate.fromLngLat({lng: coord[0], lat: coord[1]}, 0));
12-
out.push([p]);
12+
p.x = Math.round(p.x);
13+
p.y = Math.round(p.y);
14+
return p;
15+
}
16+
17+
function convertPoint(coord, canonical, out) {
18+
out.push([getPoint(coord, canonical)]);
1319
}
1420

1521
function convertPoints(coords, canonical, out) {
@@ -18,14 +24,17 @@ function convertPoints(coords, canonical, out) {
1824
}
1925
}
2026

27+
function convertLine(line, canonical, out) {
28+
const l = [];
29+
for (let i = 0; i < line.length; i++) {
30+
l.push(getPoint(line[i], canonical));
31+
}
32+
out.push(l);
33+
}
34+
2135
function convertLines(lines, canonical, out) {
2236
for (let i = 0; i < lines.length; i++) {
23-
const geom = [];
24-
const ring = lines[i];
25-
for (let j = 0; j < ring.length; j++) {
26-
convertPoint(ring[j], canonical, geom);
27-
}
28-
out.push(geom);
37+
convertLine(lines[i], canonical, out);
2938
}
3039
}
3140

@@ -38,15 +47,17 @@ function getGeometry(feature, geometry, canonical) {
3847
if (type === 'Point') {
3948
convertPoint(coords, canonical, feature.geometry);
4049
} else if (type === 'MultiPoint') {
50+
feature.type = 'Point';
4151
convertPoints(coords, canonical, feature.geometry);
4252
} else if (type === 'LineString') {
43-
convertPoints(coords, canonical, feature.geometry);
53+
convertLine(coords, canonical, feature.geometry);
4454
} else if (type === 'MultiLineString') {
55+
feature.type = 'LineString';
4556
convertLines(coords, canonical, feature.geometry);
4657
} else if (type === 'Polygon') {
4758
convertLines(coords, canonical, feature.geometry);
48-
4959
} else if (type === 'MultiPolygon') {
60+
feature.type = 'Polygon';
5061
for (let i = 0; i < coords.length; i++) {
5162
const polygon = [];
5263
convertLines(coords[i], canonical, polygon);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"expression": ["within", {
3+
"type": "MultiPolygon",
4+
"coordinates": [[[[0, 0], [0, 5], [5, 5], [5, 0], [0, 0]]],
5+
[[[0, 0], [-3, 0], [-3, -3], [0, -3], [0, 0]]]]
6+
}],
7+
"inputs": [[{
8+
"zoom": 3,
9+
"canonicalID": {
10+
"z": 3,
11+
"x": 3,
12+
"y": 3
13+
}
14+
}, {
15+
"geometry": {
16+
"type": "LineString",
17+
"coordinates": [[3, 3], [4, 1]]
18+
}
19+
}], [{
20+
"zoom": 3,
21+
"canonicalID": {
22+
"z": 3,
23+
"x": 3,
24+
"y": 3
25+
}
26+
}, {
27+
"geometry": {
28+
"type": "LineString",
29+
"coordinates": [[3, 3], [-2, -2]]
30+
}
31+
}], [{
32+
"zoom": 3,
33+
"canonicalID": {
34+
"z": 3,
35+
"x": 3,
36+
"y": 3
37+
}
38+
}, {
39+
"geometry": {
40+
"type": "LineString",
41+
"coordinates": [[0, 0], [2, 2]]
42+
}
43+
}], [{
44+
"zoom": 3,
45+
"canonicalID": {
46+
"z": 3,
47+
"x": 3,
48+
"y": 3
49+
}
50+
}, {
51+
"geometry": {
52+
"type": "LineString",
53+
"coordinates": [[1, 3], [-2, -2]]
54+
}
55+
}], [{
56+
"zoom": 3,
57+
"canonicalID": {
58+
"z": 3,
59+
"x": 3,
60+
"y": 3
61+
}
62+
}, {
63+
"geometry": {
64+
"type": "LineString",
65+
"coordinates": [[-1, -1], [-2, -2]]
66+
}
67+
}]],
68+
"expected": {
69+
"compiled": {
70+
"type": "boolean",
71+
"isFeatureConstant": false,
72+
"isZoomConstant": true,
73+
"result": "success"
74+
},
75+
"outputs": [true, false, false, false, true],
76+
"serialized": ["within", {
77+
"coordinates": [[[[0, 0], [0, 5], [5, 5], [5, 0], [0, 0]]], [[[0, 0], [-3, 0], [-3, -3], [0, -3], [0, 0]]]],
78+
"type": "MultiPolygon"
79+
}]
80+
}
81+
}

0 commit comments

Comments
 (0)