@mapbox/mapbox-gl-style-spec
Version:
a specification for mapbox gl styles
281 lines (243 loc) • 10.6 kB
text/typescript
import {isValue} from '../values';
import {BooleanType} from '../types';
import {updateBBox, boxWithinBox, pointWithinPolygon, segmentIntersectSegment} from '../../util/geometry_util';
import type {Type} from '../types';
import type {Expression, SerializedExpression} from '../expression';
import type ParsingContext from '../parsing_context';
import type EvaluationContext from '../evaluation_context';
import type Point from '@mapbox/point-geometry';
import type {CanonicalTileID} from '../../types/tile_id';
import type {BBox} from '../../util/geometry_util';
type GeoJSONPolygons = GeoJSON.Polygon | GeoJSON.MultiPolygon;
const EXTENT = 8192;
function mercatorXfromLng(lng: number) {
return (180 + lng) / 360;
}
function mercatorYfromLat(lat: number) {
return (180 - (180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)))) / 360;
}
function getTileCoordinates(p: GeoJSON.Position, 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 pointWithinPolygons(point: GeoJSON.Position, polygons: Array<Array<Array<GeoJSON.Position>>>) {
for (let i = 0; i < polygons.length; i++) {
if (pointWithinPolygon(point, polygons[i])) return true;
}
return false;
}
function lineIntersectPolygon(p1: GeoJSON.Position, p2: GeoJSON.Position, polygon: Array<Array<GeoJSON.Position>>) {
for (const ring of polygon) {
// loop through every edge of the ring
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;
}
}
}
return false;
}
function lineStringWithinPolygon(line: Array<GeoJSON.Position>, polygon: Array<Array<GeoJSON.Position>>) {
// First, check if geometry points of line segments are all inside polygon
for (let i = 0; i < line.length; ++i) {
if (!pointWithinPolygon(line[i], polygon)) {
return false;
}
}
// Second, check if there is line segment intersecting polygon edge
for (let i = 0; i < line.length - 1; ++i) {
if (lineIntersectPolygon(line[i], line[i + 1], polygon)) {
return false;
}
}
return true;
}
function lineStringWithinPolygons(line: Array<GeoJSON.Position>, polygons: Array<Array<Array<GeoJSON.Position>>>) {
for (let i = 0; i < polygons.length; i++) {
if (lineStringWithinPolygon(line, polygons[i])) return true;
}
return false;
}
function getTilePolygon(coordinates: Array<Array<GeoJSON.Position>>, bbox: BBox, canonical: CanonicalTileID) {
const polygon = [];
for (let i = 0; i < coordinates.length; i++) {
const ring = [];
for (let j = 0; j < coordinates[i].length; j++) {
const coord = getTileCoordinates(coordinates[i][j], canonical);
updateBBox(bbox, coord);
ring.push(coord);
}
polygon.push(ring);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return polygon;
}
function getTilePolygons(coordinates: Array<Array<Array<GeoJSON.Position>>>, bbox: BBox, canonical: CanonicalTileID) {
const polygons = [];
for (let i = 0; i < coordinates.length; i++) {
const polygon = getTilePolygon(coordinates[i], bbox, canonical);
polygons.push(polygon);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return polygons;
}
function updatePoint(p: GeoJSON.Position, bbox: BBox, polyBBox: Array<number>, worldSize: number) {
if (p[0] < polyBBox[0] || p[0] > polyBBox[2]) {
const halfWorldSize = worldSize * 0.5;
let shift = (p[0] - polyBBox[0] > halfWorldSize) ? -worldSize : (polyBBox[0] - p[0] > halfWorldSize) ? worldSize : 0;
if (shift === 0) {
shift = (p[0] - polyBBox[2] > halfWorldSize) ? -worldSize : (polyBBox[2] - p[0] > halfWorldSize) ? worldSize : 0;
}
p[0] += shift;
}
updateBBox(bbox, p);
}
function resetBBox(bbox: BBox) {
bbox[0] = bbox[1] = Infinity;
bbox[2] = bbox[3] = -Infinity;
}
function getTilePoints(geometry: Array<Array<Point>> | null | undefined, pointBBox: BBox, polyBBox: Array<number>, canonical: CanonicalTileID) {
const worldSize = Math.pow(2, canonical.z) * EXTENT;
const shifts = [canonical.x * EXTENT, canonical.y * EXTENT];
const tilePoints = [];
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
if (!geometry) return tilePoints;
for (const points of geometry) {
for (const point of points) {
const p = [point.x + shifts[0], point.y + shifts[1]];
updatePoint(p, pointBBox, polyBBox, worldSize);
tilePoints.push(p);
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return tilePoints;
}
function getTileLines(geometry: Array<Array<Point>> | null | undefined, lineBBox: BBox, polyBBox: Array<number>, canonical: CanonicalTileID) {
const worldSize = Math.pow(2, canonical.z) * EXTENT;
const shifts = [canonical.x * EXTENT, canonical.y * EXTENT];
const tileLines: Array<Array<GeoJSON.Position>> = [];
if (!geometry) return tileLines;
for (const line of geometry) {
const tileLine = [];
for (const point of line) {
const p: GeoJSON.Position = [point.x + shifts[0], point.y + shifts[1]];
updateBBox(lineBBox, p);
tileLine.push(p);
}
tileLines.push(tileLine);
}
if (lineBBox[2] - lineBBox[0] <= worldSize / 2) {
resetBBox(lineBBox);
for (const line of tileLines) {
for (const p of line) {
updatePoint(p, lineBBox, polyBBox, worldSize);
}
}
}
return tileLines;
}
function pointsWithinPolygons(ctx: EvaluationContext, polygonGeometry: GeoJSONPolygons) {
const pointBBox: BBox = [Infinity, Infinity, -Infinity, -Infinity];
const polyBBox: BBox = [Infinity, Infinity, -Infinity, -Infinity];
const canonical = ctx.canonicalID();
if (!canonical) {
return false;
}
if (polygonGeometry.type === 'Polygon') {
const tilePolygon = getTilePolygon(polygonGeometry.coordinates, polyBBox, canonical);
const tilePoints = getTilePoints(ctx.geometry(), pointBBox, polyBBox, canonical);
if (!boxWithinBox(pointBBox, polyBBox)) return false;
for (const point of tilePoints) {
if (!pointWithinPolygon(point, tilePolygon)) return false;
}
}
if (polygonGeometry.type === 'MultiPolygon') {
const tilePolygons = getTilePolygons(polygonGeometry.coordinates, polyBBox, canonical);
const tilePoints = getTilePoints(ctx.geometry(), pointBBox, polyBBox, canonical);
if (!boxWithinBox(pointBBox, polyBBox)) return false;
for (const point of tilePoints) {
if (!pointWithinPolygons(point, tilePolygons)) return false;
}
}
return true;
}
function linesWithinPolygons(ctx: EvaluationContext, polygonGeometry: GeoJSONPolygons) {
const lineBBox: BBox = [Infinity, Infinity, -Infinity, -Infinity];
const polyBBox: BBox = [Infinity, Infinity, -Infinity, -Infinity];
const canonical = ctx.canonicalID();
if (!canonical) {
return false;
}
if (polygonGeometry.type === 'Polygon') {
const tilePolygon = getTilePolygon(polygonGeometry.coordinates, polyBBox, canonical);
const tileLines = getTileLines(ctx.geometry(), lineBBox, polyBBox, canonical);
if (!boxWithinBox(lineBBox, polyBBox)) return false;
for (const line of tileLines) {
if (!lineStringWithinPolygon(line, tilePolygon)) return false;
}
}
if (polygonGeometry.type === 'MultiPolygon') {
const tilePolygons = getTilePolygons(polygonGeometry.coordinates, polyBBox, canonical);
const tileLines = getTileLines(ctx.geometry(), lineBBox, polyBBox, canonical);
if (!boxWithinBox(lineBBox, polyBBox)) return false;
for (const line of tileLines) {
if (!lineStringWithinPolygons(line, tilePolygons)) return false;
}
}
return true;
}
class Within implements Expression {
type: Type;
geojson: GeoJSON.GeoJSON;
geometries: GeoJSONPolygons;
constructor(geojson: GeoJSON.GeoJSON, geometries: GeoJSONPolygons) {
this.type = BooleanType;
this.geojson = geojson;
this.geometries = geometries;
}
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Within | void {
if (args.length !== 2)
return context.error(`'within' expression requires exactly one argument, but found ${args.length - 1} instead.`);
if (isValue(args[1])) {
const geojson = args[1] as GeoJSON.GeoJSON;
if (geojson.type === 'FeatureCollection') {
for (let i = 0; i < geojson.features.length; ++i) {
const type = geojson.features[i].geometry.type;
if (type === 'Polygon' || type === 'MultiPolygon') {
return new Within(geojson, geojson.features[i].geometry as GeoJSONPolygons);
}
}
} else if (geojson.type === 'Feature') {
const type = geojson.geometry.type;
if (type === 'Polygon' || type === 'MultiPolygon') {
return new Within(geojson, geojson.geometry);
}
} else if (geojson.type === 'Polygon' || geojson.type === 'MultiPolygon') {
return new Within(geojson, geojson);
}
}
return context.error(`'within' expression requires valid geojson object that contains polygon geometry type.`);
}
evaluate(ctx: EvaluationContext): boolean {
if (ctx.geometry() != null && ctx.canonicalID() != null) {
if (ctx.geometryType() === 'Point') {
return pointsWithinPolygons(ctx, this.geometries);
} else if (ctx.geometryType() === 'LineString') {
return linesWithinPolygons(ctx, this.geometries);
}
}
return false;
}
eachChild() {}
outputDefined(): boolean {
return true;
}
serialize(): SerializedExpression {
return ["within", this.geojson];
}
}
export default Within;