Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce distance expression #10616

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
553 changes: 553 additions & 0 deletions src/style-spec/expression/definitions/distance.js

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/style-spec/expression/definitions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import FormatExpression from './format.js';
import ImageExpression from './image.js';
import Length from './length.js';
import Within from './within.js';
import Distance from './distance.js';

import type {Varargs} from '../compound_expression.js';
import type {ExpressionRegistry} from '../expression.js';
Expand Down Expand Up @@ -85,7 +86,8 @@ const expressions: ExpressionRegistry = {
'to-number': Coercion,
'to-string': Coercion,
'var': Var,
'within': Within
'within': Within,
'distance': Distance
};

function rgba(ctx, [r, g, b, a]) {
Expand Down
84 changes: 5 additions & 79 deletions src/style-spec/expression/definitions/within.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,13 @@ import type {Expression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {GeoJSON, GeoJSONPolygon, GeoJSONMultiPolygon} from '@mapbox/geojson-types';
import Point from '@mapbox/point-geometry';
import type {CanonicalTileID} from '../../../source/tile_id.js';
import {updateBBox, boxWithinBox, pointWithinPolygon, segmentIntersectSegment} from '../../util/geometry_util.js';

type GeoJSONPolygons =| GeoJSONPolygon | GeoJSONMultiPolygon;

// minX, minY, maxX, maxY
type BBox = [number, number, number, number];
const EXTENT = 8192;

function updateBBox(bbox: BBox, coord: Point) {
bbox[0] = Math.min(bbox[0], coord[0]);
bbox[1] = Math.min(bbox[1], coord[1]);
bbox[2] = Math.max(bbox[2], coord[0]);
bbox[3] = Math.max(bbox[3], coord[1]);
}

function mercatorXfromLng(lng: number) {
return (180 + lng) / 360;
}
Expand All @@ -31,92 +22,27 @@ function mercatorYfromLat(lat: number) {
return (180 - (180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)))) / 360;
}

function boxWithinBox(bbox1: BBox, bbox2: BBox) {
if (bbox1[0] <= bbox2[0]) return false;
if (bbox1[2] >= bbox2[2]) return false;
if (bbox1[1] <= bbox2[1]) return false;
if (bbox1[3] >= bbox2[3]) return false;
return true;
}

function getTileCoordinates(p, canonical: CanonicalTileID) {
const x = mercatorXfromLng(p[0]);
const y = mercatorYfromLat(p[1]);
const tilesAtZoom = Math.pow(2, canonical.z);
return [Math.round(x * tilesAtZoom * EXTENT), Math.round(y * tilesAtZoom * EXTENT)];
}

function onBoundary(p, p1, p2) {
const x1 = p[0] - p1[0];
const y1 = p[1] - p1[1];
const x2 = p[0] - p2[0];
const y2 = p[1] - p2[1];
return (x1 * y2 - x2 * y1 === 0) && (x1 * x2 <= 0) && (y1 * y2 <= 0);
}

function rayIntersect(p, p1, p2) {
return ((p1[1] > p[1]) !== (p2[1] > p[1])) && (p[0] < (p2[0] - p1[0]) * (p[1] - p1[1]) / (p2[1] - p1[1]) + p1[0]);
}

// ray casting algorithm for detecting if point is in polygon
function pointWithinPolygon(point, rings) {
let inside = false;
for (let i = 0, len = rings.length; i < len; i++) {
const ring = rings[i];
for (let j = 0, len2 = ring.length; j < len2 - 1; j++) {
if (onBoundary(point, ring[j], ring[j + 1])) return false;
if (rayIntersect(point, ring[j], ring[j + 1])) inside = !inside;
}
}
return inside;
}

function pointWithinPolygons(point, polygons) {
for (let i = 0; i < polygons.length; i++) {
if (pointWithinPolygon(point, polygons[i])) return true;
}
return false;
}

function perp(v1, v2) {
return (v1[0] * v2[1] - v1[1] * v2[0]);
}

// check if p1 and p2 are in different sides of line segment q1->q2
function twoSided(p1, p2, q1, q2) {
// q1->p1 (x1, y1), q1->p2 (x2, y2), q1->q2 (x3, y3)
const x1 = p1[0] - q1[0];
const y1 = p1[1] - q1[1];
const x2 = p2[0] - q1[0];
const y2 = p2[1] - q1[1];
const x3 = q2[0] - q1[0];
const y3 = q2[1] - q1[1];
const det1 = (x1 * y3 - x3 * y1);
const det2 = (x2 * y3 - x3 * y2);
if ((det1 > 0 && det2 < 0) || (det1 < 0 && det2 > 0)) return true;
return false;
}
// a, b are end points for line segment1, c and d are end points for line segment2
function lineIntersectLine(a, b, c, d) {
// check if two segments are parallel or not
// precondition is end point a, b is inside polygon, if line a->b is
// parallel to polygon edge c->d, then a->b won't intersect with c->d
const vectorP = [b[0] - a[0], b[1] - a[1]];
const vectorQ = [d[0] - c[0], d[1] - c[1]];
if (perp(vectorQ, vectorP) === 0) return false;

// If lines are intersecting with each other, the relative location should be:
// a and b lie in different sides of segment c->d
// c and d lie in different sides of segment a->b
if (twoSided(a, b, c, d) && twoSided(c, d, a, b)) return true;
return false;
}

function lineIntersectPolygon(p1, p2, polygon) {
for (const ring of polygon) {
// loop through every edge of the ring
for (let j = 0; j < ring.length - 1; ++j) {
if (lineIntersectLine(p1, p2, ring[j], ring[j + 1])) {
for (let j = 0, len = ring.length, k = len - 1; j < len; k = j++) {
const q1 = ring[k];
const q2 = ring[j];
if (segmentIntersectSegment(p1, p2, q1, q2)) {
return true;
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/style-spec/expression/is_constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import CompoundExpression from './compound_expression.js';
import Within from './definitions/within.js';
import Distance from './definitions/distance.js';
import type {Expression} from './expression.js';

function isFeatureConstant(e: Expression) {
Expand All @@ -27,6 +28,10 @@ function isFeatureConstant(e: Expression) {
return false;
}

if (e instanceof Distance) {
return false;
}

let result = true;
e.eachChild(arg => {
if (result && !isFeatureConstant(arg)) { result = false; }
Expand Down
3 changes: 3 additions & 0 deletions src/style-spec/expression/parsing_context.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import EvaluationContext from './evaluation_context.js';
import CompoundExpression from './compound_expression.js';
import CollatorExpression from './definitions/collator.js';
import Within from './definitions/within.js';
import Distance from './definitions/distance.js';
import {isGlobalPropertyConstant, isFeatureConstant} from './is_constant.js';
import Var from './definitions/var.js';

Expand Down Expand Up @@ -204,6 +205,8 @@ function isConstant(expression: Expression) {
return false;
} else if (expression instanceof Within) {
return false;
} else if (expression instanceof Distance) {
return false;
}

const isTypeAnnotation = expression instanceof Coercion ||
Expand Down
3 changes: 1 addition & 2 deletions src/style-spec/feature_filter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function compare(a, b) {

function geometryNeeded(filter) {
if (!Array.isArray(filter)) return false;
if (filter[0] === 'within') return true;
if (filter[0] === 'within' || filter[0] === 'distance') return true;
for (let index = 1; index < filter.length; index++) {
if (geometryNeeded(filter[index])) return true;
}
Expand All @@ -123,7 +123,6 @@ function convertFilter(filter: ?Array<any>): mixed {
op === '!in' ? convertNegation(convertInOp(filter[1], filter.slice(2))) :
op === 'has' ? convertHasOp(filter[1]) :
op === '!has' ? convertNegation(convertHasOp(filter[1])) :
op === 'within' ? filter :
true;
return converted;
}
Expand Down
4 changes: 3 additions & 1 deletion src/style-spec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
"json-stringify-pretty-compact": "^2.0.0",
"minimist": "^1.2.5",
"rw": "^1.3.3",
"sort-object": "^0.3.2"
"sort-object": "^0.3.2",
"quickselect": "^2.0.0",
"tinyqueue": "^2.0.3"
},
"sideEffects": false
}
4 changes: 1 addition & 3 deletions src/style-spec/reference/v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -2541,9 +2541,6 @@
},
"!has": {
"doc": "`[\"!has\", key]` `feature[key]` does not exist"
},
"within": {
"doc": "`[\"within\", object]` feature geometry is within object geometry"
}
},
"doc": "The filter operator."
Expand Down Expand Up @@ -3475,6 +3472,7 @@
"group": "Math",
"sdk-support": {
"basic functionality": {
"js": "2.3.0",
"android": "9.2.0",
"ios": "5.9.0",
"macos": "0.16.0"
Expand Down
Loading