@mapbox/mapbox-gl-style-spec 13.28.0-beta.1 → 14.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +0 -11
- package/bin/gl-style-composite.js +2 -2
- package/bin/gl-style-format.js +2 -2
- package/bin/gl-style-migrate.js +2 -2
- package/bin/gl-style-validate.js +2 -2
- package/data/extent.js +18 -0
- package/deref.js +1 -1
- package/diff.js +36 -15
- package/dist/index.cjs +5810 -1995
- package/dist/index.cjs.map +1 -1
- package/dist/index.es.js +5808 -1991
- package/dist/index.es.js.map +1 -1
- package/expression/compound_expression.js +2 -1
- package/expression/definitions/assertion.js +2 -2
- package/expression/definitions/coercion.js +54 -15
- package/expression/definitions/comparison.js +2 -0
- package/expression/definitions/distance.js +597 -0
- package/expression/definitions/format.js +2 -2
- package/expression/definitions/image.js +34 -14
- package/expression/definitions/index.js +122 -8
- package/expression/definitions/interpolate.js +1 -1
- package/expression/definitions/match.js +2 -2
- package/expression/definitions/within.js +21 -92
- package/expression/evaluation_context.js +12 -1
- package/expression/index.js +74 -43
- package/expression/is_constant.js +19 -1
- package/expression/parsing_context.js +20 -16
- package/expression/types/formatted.js +2 -2
- package/expression/types/resolved_image.js +19 -8
- package/expression/types.js +2 -1
- package/expression/values.js +25 -0
- package/feature_filter/convert.js +1 -1
- package/feature_filter/index.js +4 -4
- package/flow-typed/cheap-ruler.js +25 -0
- package/flow-typed/geojson.js +8 -7
- package/flow-typed/gl-matrix.js +4 -2
- package/flow-typed/kdbush.js +9 -0
- package/function/convert.js +23 -12
- package/group_by_layout.js +2 -2
- package/migrate/expressions.js +3 -0
- package/package.json +5 -2
- package/reference/v8.json +1772 -112
- package/style-spec.js +4 -3
- package/types.js +190 -24
- package/util/color.js +31 -0
- package/util/geometry_util.js +145 -0
- package/util/properties.js +10 -2
- package/util/random.js +12 -0
- package/validate/validate.js +17 -7
- package/validate/validate_array.js +1 -1
- package/validate/validate_filter.js +4 -12
- package/validate/validate_function.js +2 -2
- package/validate/validate_import.js +31 -0
- package/validate/validate_layer.js +3 -2
- package/validate/validate_lights.js +84 -0
- package/validate/validate_model.js +38 -0
- package/validate/validate_property.js +17 -3
- package/validate/validate_source.js +3 -2
- package/validate/validate_style.js +29 -0
- package/validate_mapbox_api_supported.js +55 -11
- package/validate_style.js +4 -0
- package/validate_style.min.js +11 -19
- package/visit.js +3 -2
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import {isValue} from "../values.js";
|
|
4
|
+
import type {Type} from "../types.js";
|
|
5
|
+
import {NumberType} from "../types.js";
|
|
6
|
+
import type {Expression} from "../expression.js";
|
|
7
|
+
import type ParsingContext from "../parsing_context.js";
|
|
8
|
+
import type EvaluationContext from "../evaluation_context.js";
|
|
9
|
+
import type {
|
|
10
|
+
GeoJSON,
|
|
11
|
+
GeoJSONPosition,
|
|
12
|
+
GeoJSONPoint,
|
|
13
|
+
GeoJSONMultiPoint,
|
|
14
|
+
GeoJSONLineString,
|
|
15
|
+
GeoJSONMultiLineString,
|
|
16
|
+
GeoJSONPolygon,
|
|
17
|
+
GeoJSONMultiPolygon
|
|
18
|
+
} from "@mapbox/geojson-types";
|
|
19
|
+
import type {CanonicalTileID} from '../../../source/tile_id.js';
|
|
20
|
+
import {classifyRings, updateBBox, boxWithinBox, pointWithinPolygon, segmentIntersectSegment} from '../../util/geometry_util.js';
|
|
21
|
+
import type {BBox} from '../../util/geometry_util.js';
|
|
22
|
+
import CheapRuler from "cheap-ruler";
|
|
23
|
+
import Point from "@mapbox/point-geometry";
|
|
24
|
+
import TinyQueue from "tinyqueue";
|
|
25
|
+
import EXTENT from "../../data/extent.js";
|
|
26
|
+
|
|
27
|
+
type DistanceGeometry =
|
|
28
|
+
| GeoJSONPoint
|
|
29
|
+
| GeoJSONMultiPoint
|
|
30
|
+
| GeoJSONLineString
|
|
31
|
+
| GeoJSONMultiLineString
|
|
32
|
+
| GeoJSONPolygon
|
|
33
|
+
| GeoJSONMultiPolygon;
|
|
34
|
+
|
|
35
|
+
// Inclusive index range for multipoint or linestring container
|
|
36
|
+
type IndexRange = [number, number];
|
|
37
|
+
type DistPair = { dist: number, range1: IndexRange, range2: IndexRange };
|
|
38
|
+
function compareMax(a: DistPair, b: DistPair) {
|
|
39
|
+
return b.dist - a.dist;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const MIN_POINT_SIZE = 100;
|
|
43
|
+
const MIN_LINE_POINT_SIZE = 50;
|
|
44
|
+
|
|
45
|
+
function isDefaultBBOX(bbox: BBox) {
|
|
46
|
+
const defualtBBox = [Infinity, Infinity, -Infinity, -Infinity];
|
|
47
|
+
if (defualtBBox.length !== bbox.length) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
for (let i = 0; i < defualtBBox.length; i++) {
|
|
51
|
+
if (defualtBBox[i] !== bbox[i]) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getRangeSize(range: IndexRange) {
|
|
59
|
+
return range[1] - range[0] + 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isRangeSafe(range: IndexRange, threshold: number) {
|
|
63
|
+
const ret = range[1] >= range[0] && range[1] < threshold;
|
|
64
|
+
if (!ret) {
|
|
65
|
+
console.warn("Distance Expression: Index is out of range");
|
|
66
|
+
}
|
|
67
|
+
return ret;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Split the point set(points or linestring) into two halves, using IndexRange to do in-place splitting.
|
|
71
|
+
// If geometry is a line, the last point(here is the second index) of range1 needs to be included as the first point(here is the first index) of range2.
|
|
72
|
+
// If geometry are points, just split the points equally(if possible) into two new point sets(here are two index ranges).
|
|
73
|
+
function splitRange(range: IndexRange, isLine: boolean) {
|
|
74
|
+
if (range[0] > range[1]) return [null, null];
|
|
75
|
+
const size = getRangeSize(range);
|
|
76
|
+
if (isLine) {
|
|
77
|
+
if (size === 2) {
|
|
78
|
+
return [range, null];
|
|
79
|
+
}
|
|
80
|
+
const size1 = Math.floor(size / 2);
|
|
81
|
+
const range1: IndexRange = [range[0], range[0] + size1];
|
|
82
|
+
const range2: IndexRange = [range[0] + size1, range[1]];
|
|
83
|
+
return [range1, range2];
|
|
84
|
+
} else {
|
|
85
|
+
if (size === 1) {
|
|
86
|
+
return [range, null];
|
|
87
|
+
}
|
|
88
|
+
const size1 = Math.floor(size / 2) - 1;
|
|
89
|
+
const range1: IndexRange = [range[0], range[0] + size1];
|
|
90
|
+
const range2: IndexRange = [range[0] + size1 + 1, range[1]];
|
|
91
|
+
return [range1, range2];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getBBox(pointSets: Array<GeoJSONPosition>, range: IndexRange) {
|
|
96
|
+
const bbox = [Infinity, Infinity, -Infinity, -Infinity];
|
|
97
|
+
if (!isRangeSafe(range, pointSets.length)) return bbox;
|
|
98
|
+
for (let i = range[0]; i <= range[1]; ++i) {
|
|
99
|
+
updateBBox(bbox, pointSets[i]);
|
|
100
|
+
}
|
|
101
|
+
return bbox;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getPolygonBBox(polygon: Array<Array<GeoJSONPosition>>) {
|
|
105
|
+
const bbox = [Infinity, Infinity, -Infinity, -Infinity];
|
|
106
|
+
for (let i = 0; i < polygon.length; ++i) {
|
|
107
|
+
for (let j = 0; j < polygon[i].length; ++j) {
|
|
108
|
+
updateBBox(bbox, polygon[i][j]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return bbox;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Calculate the distance between two bounding boxes.
|
|
115
|
+
// Calculate the delta in x and y direction, and use two fake points {0.0, 0.0} and {dx, dy} to calculate the distance.
|
|
116
|
+
// Distance will be 0.0 if bounding box are overlapping.
|
|
117
|
+
function bboxToBBoxDistance(bbox1: BBox, bbox2: BBox, ruler: CheapRuler) {
|
|
118
|
+
if (isDefaultBBOX(bbox1) || isDefaultBBOX(bbox2)) {
|
|
119
|
+
return NaN;
|
|
120
|
+
}
|
|
121
|
+
let dx = 0.0;
|
|
122
|
+
let dy = 0.0;
|
|
123
|
+
// bbox1 in left side
|
|
124
|
+
if (bbox1[2] < bbox2[0]) {
|
|
125
|
+
dx = bbox2[0] - bbox1[2];
|
|
126
|
+
}
|
|
127
|
+
// bbox1 in right side
|
|
128
|
+
if (bbox1[0] > bbox2[2]) {
|
|
129
|
+
dx = bbox1[0] - bbox2[2];
|
|
130
|
+
}
|
|
131
|
+
// bbox1 in above side
|
|
132
|
+
if (bbox1[1] > bbox2[3]) {
|
|
133
|
+
dy = bbox1[1] - bbox2[3];
|
|
134
|
+
}
|
|
135
|
+
// bbox1 in down side
|
|
136
|
+
if (bbox1[3] < bbox2[1]) {
|
|
137
|
+
dy = bbox2[1] - bbox1[3];
|
|
138
|
+
}
|
|
139
|
+
return ruler.distance([0.0, 0.0], [dx, dy]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function lngFromMercatorX(x: number): number {
|
|
143
|
+
return x * 360 - 180;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function latFromMercatorY(y: number): number {
|
|
147
|
+
const y2 = 180 - y * 360;
|
|
148
|
+
return 360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getLngLatPoint(coord: Point, canonical: CanonicalTileID) {
|
|
152
|
+
const tilesAtZoom = Math.pow(2, canonical.z);
|
|
153
|
+
const x = (coord.x / EXTENT + canonical.x) / tilesAtZoom;
|
|
154
|
+
const y = (coord.y / EXTENT + canonical.y) / tilesAtZoom;
|
|
155
|
+
return [lngFromMercatorX(x), latFromMercatorY(y)];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getLngLatPoints(coordinates: Array<Point>, canonical: CanonicalTileID) {
|
|
159
|
+
const coords = [];
|
|
160
|
+
for (let i = 0; i < coordinates.length; ++i) {
|
|
161
|
+
coords.push(getLngLatPoint(coordinates[i], canonical));
|
|
162
|
+
}
|
|
163
|
+
return coords;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function pointToLineDistance(point: GeoJSONPosition, line: Array<GeoJSONPosition>, ruler: CheapRuler) {
|
|
167
|
+
const nearestPoint = ruler.pointOnLine(line, point).point;
|
|
168
|
+
return ruler.distance(point, nearestPoint);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function pointsToLineDistance(points: Array<GeoJSONPosition>, rangeA: IndexRange, line: Array<GeoJSONPosition>, rangeB: IndexRange, ruler: CheapRuler) {
|
|
172
|
+
const subLine = line.slice(rangeB[0], rangeB[1] + 1);
|
|
173
|
+
let dist = Infinity;
|
|
174
|
+
for (let i = rangeA[0]; i <= rangeA[1]; ++i) {
|
|
175
|
+
if ((dist = Math.min(dist, pointToLineDistance(points[i], subLine, ruler))) === 0.0) return 0.0;
|
|
176
|
+
}
|
|
177
|
+
return dist;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// precondition is two segments are not intersecting with each other
|
|
181
|
+
function segmentToSegmentDistance(p1: GeoJSONPosition, p2: GeoJSONPosition, q1: GeoJSONPosition, q2: GeoJSONPosition, ruler: CheapRuler) {
|
|
182
|
+
const dist1 = Math.min(
|
|
183
|
+
ruler.pointToSegmentDistance(p1, q1, q2),
|
|
184
|
+
ruler.pointToSegmentDistance(p2, q1, q2)
|
|
185
|
+
);
|
|
186
|
+
const dist2 = Math.min(
|
|
187
|
+
ruler.pointToSegmentDistance(q1, p1, p2),
|
|
188
|
+
ruler.pointToSegmentDistance(q2, p1, p2)
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
return Math.min(dist1, dist2);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function lineToLineDistance(line1: Array<GeoJSONPosition>, range1: IndexRange, line2: Array<GeoJSONPosition>, range2: IndexRange, ruler: CheapRuler) {
|
|
195
|
+
if (!isRangeSafe(range1, line1.length) || !isRangeSafe(range2, line2.length)) {
|
|
196
|
+
return NaN;
|
|
197
|
+
}
|
|
198
|
+
let dist = Infinity;
|
|
199
|
+
for (let i = range1[0]; i < range1[1]; ++i) {
|
|
200
|
+
for (let j = range2[0]; j < range2[1]; ++j) {
|
|
201
|
+
if (segmentIntersectSegment(line1[i], line1[i + 1], line2[j], line2[j + 1])) return 0.0;
|
|
202
|
+
dist = Math.min(dist, segmentToSegmentDistance(line1[i], line1[i + 1], line2[j], line2[j + 1], ruler));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return dist;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function pointsToPointsDistance(pointSet1: Array<GeoJSONPosition>, range1: IndexRange, pointSet2: Array<GeoJSONPosition>, range2: IndexRange, ruler: CheapRuler) {
|
|
209
|
+
if (!isRangeSafe(range1, pointSet1.length) || !isRangeSafe(range2, pointSet2.length)) {
|
|
210
|
+
return NaN;
|
|
211
|
+
}
|
|
212
|
+
let dist = Infinity;
|
|
213
|
+
for (let i = range1[0]; i <= range1[1]; ++i) {
|
|
214
|
+
for (let j = range2[0]; j <= range2[1]; ++j) {
|
|
215
|
+
if ((dist = Math.min(dist, ruler.distance(pointSet1[i], pointSet2[j]))) === 0.0) return dist;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return dist;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function pointToPolygonDistance(point: GeoJSONPosition, polygon: Array<Array<GeoJSONPosition>>, ruler: CheapRuler) {
|
|
222
|
+
if (pointWithinPolygon(point, polygon, true /*trueOnBoundary*/)) return 0.0;
|
|
223
|
+
let dist = Infinity;
|
|
224
|
+
for (const ring of polygon) {
|
|
225
|
+
const ringLen = ring.length;
|
|
226
|
+
if (ringLen < 2) {
|
|
227
|
+
console.warn("Distance Expression: Invalid polygon!");
|
|
228
|
+
return NaN;
|
|
229
|
+
}
|
|
230
|
+
if (ring[0] !== ring[ringLen - 1]) {
|
|
231
|
+
if ((dist = Math.min(dist, ruler.pointToSegmentDistance(point, ring[ringLen - 1], ring[0]))) === 0.0) return dist;
|
|
232
|
+
}
|
|
233
|
+
if ((dist = Math.min(dist, pointToLineDistance(point, ring, ruler))) === 0.0) return dist;
|
|
234
|
+
}
|
|
235
|
+
return dist;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function lineToPolygonDistance(line: Array<GeoJSONPosition>, range: IndexRange, polygon: Array<Array<GeoJSONPosition>>, ruler: CheapRuler) {
|
|
239
|
+
if (!isRangeSafe(range, line.length)) {
|
|
240
|
+
return NaN;
|
|
241
|
+
}
|
|
242
|
+
for (let i = range[0]; i <= range[1]; ++i) {
|
|
243
|
+
if (pointWithinPolygon(line[i], polygon, true /*trueOnBoundary*/)) return 0.0;
|
|
244
|
+
}
|
|
245
|
+
let dist = Infinity;
|
|
246
|
+
for (let i = range[0]; i < range[1]; ++i) {
|
|
247
|
+
for (const ring of polygon) {
|
|
248
|
+
for (let j = 0, len = ring.length, k = len - 1; j < len; k = j++) {
|
|
249
|
+
if (segmentIntersectSegment(line[i], line[i + 1], ring[k], ring[j])) return 0.0;
|
|
250
|
+
dist = Math.min(dist, segmentToSegmentDistance(line[i], line[i + 1], ring[k], ring[j], ruler));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return dist;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function polygonIntersect(polygon1: Array<Array<GeoJSONPosition>>, polygon2: Array<Array<GeoJSONPosition>>) {
|
|
258
|
+
for (const ring of polygon1) {
|
|
259
|
+
for (let i = 0; i <= ring.length - 1; ++i) {
|
|
260
|
+
if (pointWithinPolygon(ring[i], polygon2, true /*trueOnBoundary*/)) return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function polygonToPolygonDistance(polygon1: Array<Array<GeoJSONPosition>>, polygon2: Array<Array<GeoJSONPosition>>, ruler: CheapRuler, currentMiniDist: number = Infinity) {
|
|
267
|
+
const bbox1 = getPolygonBBox(polygon1);
|
|
268
|
+
const bbox2 = getPolygonBBox(polygon2);
|
|
269
|
+
if (currentMiniDist !== Infinity && bboxToBBoxDistance(bbox1, bbox2, ruler) >= currentMiniDist) {
|
|
270
|
+
return currentMiniDist;
|
|
271
|
+
}
|
|
272
|
+
if (boxWithinBox(bbox1, bbox2)) {
|
|
273
|
+
if (polygonIntersect(polygon1, polygon2)) return 0.0;
|
|
274
|
+
} else if (polygonIntersect(polygon2, polygon1)) {
|
|
275
|
+
return 0.0;
|
|
276
|
+
}
|
|
277
|
+
let dist = currentMiniDist;
|
|
278
|
+
for (const ring1 of polygon1) {
|
|
279
|
+
for (let i = 0, len1 = ring1.length, l = len1 - 1; i < len1; l = i++) {
|
|
280
|
+
for (const ring2 of polygon2) {
|
|
281
|
+
for (let j = 0, len2 = ring2.length, k = len2 - 1; j < len2; k = j++) {
|
|
282
|
+
if (segmentIntersectSegment(ring1[l], ring1[i], ring2[k], ring2[j])) return 0.0;
|
|
283
|
+
dist = Math.min(dist, segmentToSegmentDistance(ring1[l], ring1[i], ring2[k], ring2[j], ruler));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return dist;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function updateQueue(distQueue: any, miniDist: number, ruler: CheapRuler, pointSet1: Array<GeoJSONPosition>, pointSet2: Array<GeoJSONPosition>, r1: IndexRange | null, r2: IndexRange | null) {
|
|
292
|
+
if (r1 === null || r2 === null) return;
|
|
293
|
+
const tempDist = bboxToBBoxDistance(getBBox(pointSet1, r1), getBBox(pointSet2, r2), ruler);
|
|
294
|
+
// Insert new pair to the queue if the bbox distance is less than miniDist, the pair with biggest distance will be at the top
|
|
295
|
+
if (tempDist < miniDist) distQueue.push({dist: tempDist, range1: r1, range2: r2});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Divide and conquer, the time complexity is O(n*lgn), faster than Brute force O(n*n)
|
|
299
|
+
// Most of the time, use index for in-place processing.
|
|
300
|
+
function pointSetToPolygonDistance(pointSets: Array<GeoJSONPosition>, isLine: boolean, polygon: Array<Array<GeoJSONPosition>>, ruler: CheapRuler, currentMiniDist: number = Infinity) {
|
|
301
|
+
let miniDist = Math.min(ruler.distance(pointSets[0], polygon[0][0]), currentMiniDist);
|
|
302
|
+
if (miniDist === 0.0) return miniDist;
|
|
303
|
+
const initialDistPair: DistPair = {
|
|
304
|
+
dist: 0,
|
|
305
|
+
range1: [0, pointSets.length - 1],
|
|
306
|
+
range2: [0, 0]
|
|
307
|
+
};
|
|
308
|
+
const distQueue = new TinyQueue<DistPair>([initialDistPair], compareMax);
|
|
309
|
+
|
|
310
|
+
const setThreshold = isLine ? MIN_LINE_POINT_SIZE : MIN_POINT_SIZE;
|
|
311
|
+
const polyBBox = getPolygonBBox(polygon);
|
|
312
|
+
|
|
313
|
+
while (distQueue.length) {
|
|
314
|
+
const distPair = distQueue.pop();
|
|
315
|
+
if (distPair.dist >= miniDist) continue;
|
|
316
|
+
const range = distPair.range1;
|
|
317
|
+
// In case the set size are relatively small, we could use brute-force directly
|
|
318
|
+
if (getRangeSize(range) <= setThreshold) {
|
|
319
|
+
if (!isRangeSafe(range, pointSets.length)) return NaN;
|
|
320
|
+
if (isLine) {
|
|
321
|
+
const tempDist = lineToPolygonDistance(pointSets, range, polygon, ruler);
|
|
322
|
+
if ((miniDist = Math.min(miniDist, tempDist)) === 0.0) return miniDist;
|
|
323
|
+
} else {
|
|
324
|
+
for (let i = range[0]; i <= range[1]; ++i) {
|
|
325
|
+
const tempDist = pointToPolygonDistance(pointSets[i], polygon, ruler);
|
|
326
|
+
if ((miniDist = Math.min(miniDist, tempDist)) === 0.0) return miniDist;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
const newRanges = splitRange(range, isLine);
|
|
331
|
+
if (newRanges[0] !== null) {
|
|
332
|
+
const tempDist = bboxToBBoxDistance(getBBox(pointSets, newRanges[0]), polyBBox, ruler);
|
|
333
|
+
if (tempDist < miniDist) distQueue.push({dist: tempDist, range1: newRanges[0], range2: [0, 0]});
|
|
334
|
+
}
|
|
335
|
+
if (newRanges[1] !== null) {
|
|
336
|
+
const tempDist = bboxToBBoxDistance(getBBox(pointSets, newRanges[1]), polyBBox, ruler);
|
|
337
|
+
if (tempDist < miniDist) distQueue.push({dist: tempDist, range1: newRanges[1], range2: [0, 0]});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return miniDist;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function pointSetsDistance(pointSet1: Array<GeoJSONPosition>, isLine1: boolean, pointSet2: Array<GeoJSONPosition>, isLine2: boolean, ruler: CheapRuler, currentMiniDist: number = Infinity) {
|
|
345
|
+
let miniDist = Math.min(currentMiniDist, ruler.distance(pointSet1[0], pointSet2[0]));
|
|
346
|
+
if (miniDist === 0.0) return miniDist;
|
|
347
|
+
const initialDistPair: DistPair = {
|
|
348
|
+
dist: 0,
|
|
349
|
+
range1: [0, pointSet1.length - 1],
|
|
350
|
+
range2: [0, pointSet2.length - 1]
|
|
351
|
+
};
|
|
352
|
+
const distQueue = new TinyQueue<DistPair>([initialDistPair], compareMax);
|
|
353
|
+
|
|
354
|
+
const set1Threshold = isLine1 ? MIN_LINE_POINT_SIZE : MIN_POINT_SIZE;
|
|
355
|
+
const set2Threshold = isLine2 ? MIN_LINE_POINT_SIZE : MIN_POINT_SIZE;
|
|
356
|
+
|
|
357
|
+
while (distQueue.length) {
|
|
358
|
+
const distPair = distQueue.pop();
|
|
359
|
+
if (distPair.dist >= miniDist) continue;
|
|
360
|
+
const rangeA = distPair.range1;
|
|
361
|
+
const rangeB = distPair.range2;
|
|
362
|
+
// In case the set size are relatively small, we could use brute-force directly
|
|
363
|
+
if (getRangeSize(rangeA) <= set1Threshold && getRangeSize(rangeB) <= set2Threshold) {
|
|
364
|
+
if (!isRangeSafe(rangeA, pointSet1.length) || !isRangeSafe(rangeB, pointSet2.length)) {
|
|
365
|
+
return NaN;
|
|
366
|
+
}
|
|
367
|
+
if (isLine1 && isLine2) {
|
|
368
|
+
miniDist = Math.min(miniDist, lineToLineDistance(pointSet1, rangeA, pointSet2, rangeB, ruler));
|
|
369
|
+
} else if (!isLine1 && !isLine2) {
|
|
370
|
+
miniDist = Math.min(miniDist, pointsToPointsDistance(pointSet1, rangeA, pointSet2, rangeB, ruler));
|
|
371
|
+
} else if (isLine1 && !isLine2) {
|
|
372
|
+
miniDist = Math.min(miniDist, pointsToLineDistance(pointSet2, rangeB, pointSet1, rangeA, ruler));
|
|
373
|
+
} else if (!isLine1 && isLine2) {
|
|
374
|
+
miniDist = Math.min(miniDist, pointsToLineDistance(pointSet1, rangeA, pointSet2, rangeB, ruler));
|
|
375
|
+
}
|
|
376
|
+
if (miniDist === 0.0) return miniDist;
|
|
377
|
+
} else {
|
|
378
|
+
const newRangesA = splitRange(rangeA, isLine1);
|
|
379
|
+
const newRangesB = splitRange(rangeB, isLine2);
|
|
380
|
+
updateQueue(distQueue, miniDist, ruler, pointSet1, pointSet2, newRangesA[0], newRangesB[0]);
|
|
381
|
+
updateQueue(distQueue, miniDist, ruler, pointSet1, pointSet2, newRangesA[0], newRangesB[1]);
|
|
382
|
+
updateQueue(distQueue, miniDist, ruler, pointSet1, pointSet2, newRangesA[1], newRangesB[0]);
|
|
383
|
+
updateQueue(distQueue, miniDist, ruler, pointSet1, pointSet2, newRangesA[1], newRangesB[1]);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return miniDist;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function pointSetToLinesDistance(pointSet: Array<GeoJSONPosition>, isLine: boolean, lines: Array<Array<GeoJSONPosition>>, ruler: CheapRuler, currentMiniDist: number = Infinity) {
|
|
390
|
+
let dist = currentMiniDist;
|
|
391
|
+
const bbox1 = getBBox(pointSet, [0, pointSet.length - 1]);
|
|
392
|
+
for (const line of lines) {
|
|
393
|
+
if (dist !== Infinity && bboxToBBoxDistance(bbox1, getBBox(line, [0, line.length - 1]), ruler) >= dist) continue;
|
|
394
|
+
dist = Math.min(dist, pointSetsDistance(pointSet, isLine, line, true /*isLine*/, ruler, dist));
|
|
395
|
+
if (dist === 0.0) return dist;
|
|
396
|
+
}
|
|
397
|
+
return dist;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function pointSetToPolygonsDistance(points: Array<GeoJSONPosition>, isLine: boolean, polygons: Array<Array<Array<GeoJSONPosition>>>, ruler: CheapRuler, currentMiniDist: number = Infinity) {
|
|
401
|
+
let dist = currentMiniDist;
|
|
402
|
+
const bbox1 = getBBox(points, [0, points.length - 1]);
|
|
403
|
+
for (const polygon of polygons) {
|
|
404
|
+
if (dist !== Infinity && bboxToBBoxDistance(bbox1, getPolygonBBox(polygon), ruler) >= dist) continue;
|
|
405
|
+
const tempDist = pointSetToPolygonDistance(points, isLine, polygon, ruler, dist);
|
|
406
|
+
if (isNaN(tempDist)) return tempDist;
|
|
407
|
+
if ((dist = Math.min(dist, tempDist)) === 0.0) return dist;
|
|
408
|
+
}
|
|
409
|
+
return dist;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function polygonsToPolygonsDistance(polygons1: Array<Array<Array<GeoJSONPosition>>>, polygons2: Array<Array<Array<GeoJSONPosition>>>, ruler: CheapRuler) {
|
|
413
|
+
let dist = Infinity;
|
|
414
|
+
for (const polygon1 of polygons1) {
|
|
415
|
+
for (const polygon2 of polygons2) {
|
|
416
|
+
const tempDist = polygonToPolygonDistance(polygon1, polygon2, ruler, dist);
|
|
417
|
+
if (isNaN(tempDist)) return tempDist;
|
|
418
|
+
if ((dist = Math.min(dist, tempDist)) === 0.0) return dist;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return dist;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function pointsToGeometryDistance(originGeometry: Array<Array<Point>>, canonical: CanonicalTileID, geometry: DistanceGeometry) {
|
|
425
|
+
const lngLatPoints = [];
|
|
426
|
+
for (const points of originGeometry) {
|
|
427
|
+
for (const point of points) {
|
|
428
|
+
lngLatPoints.push(getLngLatPoint(point, canonical));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const ruler = new CheapRuler(lngLatPoints[0][1], 'meters');
|
|
432
|
+
if (geometry.type === 'Point' || geometry.type === 'MultiPoint' || geometry.type === 'LineString') {
|
|
433
|
+
return pointSetsDistance(lngLatPoints, false /*isLine*/,
|
|
434
|
+
geometry.type === "Point" ? [geometry.coordinates] : geometry.coordinates,
|
|
435
|
+
geometry.type === 'LineString' /*isLine*/, ruler);
|
|
436
|
+
}
|
|
437
|
+
if (geometry.type === 'MultiLineString') {
|
|
438
|
+
return pointSetToLinesDistance(lngLatPoints, false /*isLine*/, geometry.coordinates, ruler);
|
|
439
|
+
}
|
|
440
|
+
if (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') {
|
|
441
|
+
return pointSetToPolygonsDistance(lngLatPoints, false /*isLine*/,
|
|
442
|
+
geometry.type === "Polygon" ? [geometry.coordinates] : geometry.coordinates, ruler);
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function linesToGeometryDistance(originGeometry: Array<Array<Point>>, canonical: CanonicalTileID, geometry: DistanceGeometry) {
|
|
448
|
+
const lngLatLines = [];
|
|
449
|
+
for (const line of originGeometry) {
|
|
450
|
+
const lngLatLine = [];
|
|
451
|
+
for (const point of line) {
|
|
452
|
+
lngLatLine.push(getLngLatPoint(point, canonical));
|
|
453
|
+
}
|
|
454
|
+
lngLatLines.push(lngLatLine);
|
|
455
|
+
}
|
|
456
|
+
const ruler = new CheapRuler(lngLatLines[0][0][1], 'meters');
|
|
457
|
+
if (geometry.type === 'Point' || geometry.type === 'MultiPoint' || geometry.type === 'LineString') {
|
|
458
|
+
return pointSetToLinesDistance(
|
|
459
|
+
geometry.type === "Point" ? [geometry.coordinates] : geometry.coordinates,
|
|
460
|
+
geometry.type === 'LineString' /*isLine*/, lngLatLines, ruler);
|
|
461
|
+
}
|
|
462
|
+
if (geometry.type === 'MultiLineString') {
|
|
463
|
+
let dist = Infinity;
|
|
464
|
+
for (let i = 0; i < geometry.coordinates.length; i++) {
|
|
465
|
+
const tempDist = pointSetToLinesDistance(geometry.coordinates[i], true /*isLine*/, lngLatLines, ruler, dist);
|
|
466
|
+
if (isNaN(tempDist)) return tempDist;
|
|
467
|
+
if ((dist = Math.min(dist, tempDist)) === 0.0) return dist;
|
|
468
|
+
}
|
|
469
|
+
return dist;
|
|
470
|
+
}
|
|
471
|
+
if (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') {
|
|
472
|
+
let dist = Infinity;
|
|
473
|
+
for (let i = 0; i < lngLatLines.length; i++) {
|
|
474
|
+
const tempDist = pointSetToPolygonsDistance(lngLatLines[i], true /*isLine*/,
|
|
475
|
+
geometry.type === "Polygon" ? [geometry.coordinates] : geometry.coordinates,
|
|
476
|
+
ruler, dist);
|
|
477
|
+
if (isNaN(tempDist)) return tempDist;
|
|
478
|
+
if ((dist = Math.min(dist, tempDist)) === 0.0) return dist;
|
|
479
|
+
}
|
|
480
|
+
return dist;
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function polygonsToGeometryDistance(originGeometry: Array<Array<Point>>, canonical: CanonicalTileID, geometry: DistanceGeometry) {
|
|
486
|
+
const lngLatPolygons = [];
|
|
487
|
+
for (const polygon of classifyRings(originGeometry, 0)) {
|
|
488
|
+
const lngLatPolygon = [];
|
|
489
|
+
for (let i = 0; i < polygon.length; ++i) {
|
|
490
|
+
lngLatPolygon.push(getLngLatPoints(polygon[i], canonical));
|
|
491
|
+
}
|
|
492
|
+
lngLatPolygons.push(lngLatPolygon);
|
|
493
|
+
}
|
|
494
|
+
const ruler = new CheapRuler(lngLatPolygons[0][0][0][1], 'meters');
|
|
495
|
+
if (geometry.type === 'Point' || geometry.type === 'MultiPoint' || geometry.type === 'LineString') {
|
|
496
|
+
return pointSetToPolygonsDistance(
|
|
497
|
+
geometry.type === "Point" ? [geometry.coordinates] : geometry.coordinates,
|
|
498
|
+
geometry.type === 'LineString' /*isLine*/, lngLatPolygons, ruler);
|
|
499
|
+
}
|
|
500
|
+
if (geometry.type === 'MultiLineString') {
|
|
501
|
+
let dist = Infinity;
|
|
502
|
+
for (let i = 0; i < geometry.coordinates.length; i++) {
|
|
503
|
+
const tempDist = pointSetToPolygonsDistance(geometry.coordinates[i], true /*isLine*/, lngLatPolygons, ruler, dist);
|
|
504
|
+
if (isNaN(tempDist)) return tempDist;
|
|
505
|
+
if ((dist = Math.min(dist, tempDist)) === 0.0) return dist;
|
|
506
|
+
}
|
|
507
|
+
return dist;
|
|
508
|
+
}
|
|
509
|
+
if (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') {
|
|
510
|
+
return polygonsToPolygonsDistance(
|
|
511
|
+
geometry.type === "Polygon" ? [geometry.coordinates] : geometry.coordinates,
|
|
512
|
+
lngLatPolygons, ruler);
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function isTypeValid(type: string) {
|
|
518
|
+
return (
|
|
519
|
+
type === "Point" ||
|
|
520
|
+
type === "MultiPoint" ||
|
|
521
|
+
type === "LineString" ||
|
|
522
|
+
type === "MultiLineString" ||
|
|
523
|
+
type === "Polygon" ||
|
|
524
|
+
type === "MultiPolygon"
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
class Distance implements Expression {
|
|
528
|
+
type: Type;
|
|
529
|
+
geojson: GeoJSON;
|
|
530
|
+
geometries: DistanceGeometry;
|
|
531
|
+
|
|
532
|
+
constructor(geojson: GeoJSON, geometries: DistanceGeometry) {
|
|
533
|
+
this.type = NumberType;
|
|
534
|
+
this.geojson = geojson;
|
|
535
|
+
this.geometries = geometries;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Distance {
|
|
539
|
+
if (args.length !== 2) {
|
|
540
|
+
return context.error(
|
|
541
|
+
`'distance' expression requires either one argument, but found ' ${args.length -
|
|
542
|
+
1} instead.`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
if (isValue(args[1])) {
|
|
546
|
+
const geojson = (args[1]: Object);
|
|
547
|
+
if (geojson.type === 'FeatureCollection') {
|
|
548
|
+
for (let i = 0; i < geojson.features.length; ++i) {
|
|
549
|
+
if (isTypeValid(geojson.features[i].geometry.type)) {
|
|
550
|
+
return new Distance(geojson, geojson.features[i].geometry);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} else if (geojson.type === 'Feature') {
|
|
554
|
+
if (isTypeValid(geojson.geometry.type)) {
|
|
555
|
+
return new Distance(geojson, geojson.geometry);
|
|
556
|
+
}
|
|
557
|
+
} else if (isTypeValid(geojson.type)) {
|
|
558
|
+
return new Distance(geojson, geojson);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return context.error(
|
|
562
|
+
"'distance' expression needs to be an array with format [\'Distance\', GeoJSONObj]."
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
evaluate(ctx: EvaluationContext): number | null {
|
|
567
|
+
const geometry = ctx.geometry();
|
|
568
|
+
const canonical = ctx.canonicalID();
|
|
569
|
+
if (geometry != null && canonical != null) {
|
|
570
|
+
if (ctx.geometryType() === 'Point') {
|
|
571
|
+
return pointsToGeometryDistance(geometry, canonical, this.geometries);
|
|
572
|
+
}
|
|
573
|
+
if (ctx.geometryType() === 'LineString') {
|
|
574
|
+
return linesToGeometryDistance(geometry, canonical, this.geometries);
|
|
575
|
+
}
|
|
576
|
+
if (ctx.geometryType() === 'Polygon') {
|
|
577
|
+
return polygonsToGeometryDistance(geometry, canonical, this.geometries);
|
|
578
|
+
}
|
|
579
|
+
console.warn("Distance Expression: currently only evaluates valid Point/LineString/Polygon geometries.");
|
|
580
|
+
} else {
|
|
581
|
+
console.warn("Distance Expression: requirs valid feature and canonical information.");
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
eachChild() {}
|
|
587
|
+
|
|
588
|
+
outputDefined(): boolean {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
serialize(): Array<mixed> {
|
|
593
|
+
return ['distance', this.geojson];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export default Distance;
|
|
@@ -9,7 +9,7 @@ import type EvaluationContext from '../evaluation_context.js';
|
|
|
9
9
|
import type ParsingContext from '../parsing_context.js';
|
|
10
10
|
import type {Type} from '../types.js';
|
|
11
11
|
|
|
12
|
-
type FormattedSectionExpression = {
|
|
12
|
+
export type FormattedSectionExpression = {
|
|
13
13
|
// Content of a section may be Image expression or other
|
|
14
14
|
// type of expression that is coercable to 'string'.
|
|
15
15
|
content: Expression,
|
|
@@ -84,7 +84,7 @@ export default class FormatExpression implements Expression {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
evaluate(ctx: EvaluationContext): Formatted {
|
|
87
|
-
const evaluateSection = section => {
|
|
87
|
+
const evaluateSection = (section: FormattedSectionExpression) => {
|
|
88
88
|
const evaluatedContent = section.content.evaluate(ctx);
|
|
89
89
|
if (typeOf(evaluatedContent) === ResolvedImageType) {
|
|
90
90
|
return new FormattedSection('', evaluatedContent, null, null, null);
|
|
@@ -10,35 +10,51 @@ import type {Type} from '../types.js';
|
|
|
10
10
|
|
|
11
11
|
export default class ImageExpression implements Expression {
|
|
12
12
|
type: Type;
|
|
13
|
-
|
|
13
|
+
inputPrimary: Expression;
|
|
14
|
+
inputSecondary: ?Expression;
|
|
14
15
|
|
|
15
|
-
constructor(
|
|
16
|
+
constructor(inputPrimary: Expression, inputSecondary: ?Expression) {
|
|
16
17
|
this.type = ResolvedImageType;
|
|
17
|
-
this.
|
|
18
|
+
this.inputPrimary = inputPrimary;
|
|
19
|
+
this.inputSecondary = inputSecondary;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
|
|
21
|
-
if (args.length
|
|
22
|
-
return context.error(`Expected two arguments.`);
|
|
23
|
+
if (args.length < 2) {
|
|
24
|
+
return context.error(`Expected two or more arguments.`);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
27
|
+
const namePrimary = context.parse(args[1], 1, StringType);
|
|
28
|
+
if (!namePrimary) return context.error(`No image name provided.`);
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
if (args.length === 2) {
|
|
31
|
+
return new ImageExpression(namePrimary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const nameSecondary = context.parse(args[2], 1, StringType);
|
|
35
|
+
if (!nameSecondary) return context.error(`Secondary image variant is not a string.`);
|
|
36
|
+
|
|
37
|
+
return new ImageExpression(namePrimary, nameSecondary);
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
evaluate(ctx: EvaluationContext): null | ResolvedImage {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
const value = ResolvedImage.fromString(this.inputPrimary.evaluate(ctx), this.inputSecondary ? this.inputSecondary.evaluate(ctx) : undefined);
|
|
42
|
+
if (value && ctx.availableImages) {
|
|
43
|
+
value.available = ctx.availableImages.indexOf(value.namePrimary) > -1;
|
|
44
|
+
// If there's a secondary variant, only mark it available if both are present
|
|
45
|
+
if (value.nameSecondary && value.available && ctx.availableImages) {
|
|
46
|
+
value.available = ctx.availableImages.indexOf(value.nameSecondary) > -1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
36
49
|
|
|
37
50
|
return value;
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
eachChild(fn: (_: Expression) => void) {
|
|
41
|
-
fn(this.
|
|
54
|
+
fn(this.inputPrimary);
|
|
55
|
+
if (this.inputSecondary) {
|
|
56
|
+
fn(this.inputSecondary);
|
|
57
|
+
}
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
outputDefined(): boolean {
|
|
@@ -47,6 +63,10 @@ export default class ImageExpression implements Expression {
|
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
serialize(): SerializedExpression {
|
|
50
|
-
|
|
66
|
+
if (this.inputSecondary) {
|
|
67
|
+
// $FlowIgnore
|
|
68
|
+
return ["image", this.inputPrimary.serialize(), this.inputSecondary.serialize()];
|
|
69
|
+
}
|
|
70
|
+
return ["image", this.inputPrimary.serialize()];
|
|
51
71
|
}
|
|
52
72
|
}
|