UNPKG

@mapbox/mapbox-gl-style-spec

Version:

a specification for mapbox gl styles

281 lines (243 loc) 10.6 kB
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;