maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
171 lines (153 loc) • 7.3 kB
text/typescript
import Point from '@mapbox/point-geometry';
import type {PossiblyEvaluatedPropertyValue} from './properties';
import type {StyleLayer} from '../style/style_layer';
import type {CircleBucket} from '../data/bucket/circle_bucket';
import type {LineBucket} from '../data/bucket/line_bucket';
import {polygonIntersectsBufferedPoint} from '../util/intersection_tests';
import type {IReadonlyTransform} from '../geo/transform_interface';
import type {UnwrappedTileID} from '../tile/tile_id';
export function getMaximumPaintValue(
property: string,
layer: StyleLayer,
bucket: CircleBucket<any> | LineBucket
): number {
const value = ((layer.paint as any).get(property) as PossiblyEvaluatedPropertyValue<any>).value;
if (value.kind === 'constant') {
return value.value;
} else {
return bucket.programConfigurations.get(layer.id).getMaxValue(property);
}
}
export function translateDistance(translate: [number, number]) {
return Math.sqrt(translate[0] * translate[0] + translate[1] * translate[1]);
}
/**
* @internal
* Translates a geometry by a certain pixels in tile coordinates
* @param queryGeometry - The geometry to translate in tile coordinates
* @param translate - The translation in pixels
* @param translateAnchor - The anchor of the translation
* @param bearing - The bearing of the map
* @param pixelsToTileUnits - The scale factor from pixels to tile units
* @returns the translated geometry in tile coordinates
*/
export function translate(queryGeometry: Array<Point>,
translate: [number, number],
translateAnchor: 'viewport' | 'map',
bearing: number,
pixelsToTileUnits: number): Point[] {
if (!translate[0] && !translate[1]) {
return queryGeometry;
}
const pt = Point.convert(translate)._mult(pixelsToTileUnits);
if (translateAnchor === 'viewport') {
pt._rotate(-bearing);
}
const translated: Point[] = [];
for (let i = 0; i < queryGeometry.length; i++) {
const point = queryGeometry[i];
translated.push(point.sub(pt));
}
return translated;
}
/**
* Filter out consecutive duplicate points from a line
*/
function _stripDuplicates(ring: Array<Point>): Array<Point> {
const filteredRing: Array<Point> = [];
for (let index = 0; index < ring.length; index++) {
const point = ring[index];
const prevPoint = filteredRing.at(-1);
if (index === 0 || (prevPoint && !(point.equals(prevPoint)))) {
filteredRing.push(point);
}
}
return filteredRing;
}
export function offsetLine(rings: Array<Array<Point>>, offset: number) {
const newRings: Array<Array<Point>> = [];
for (let ringIndex = 0; ringIndex < rings.length; ringIndex++) {
const ring = _stripDuplicates(rings[ringIndex]);
const newRing: Array<Point> = [];
for (let index = 0; index < ring.length; index++) {
const point = ring[index];
const prevPoint = ring[index - 1];
const nextPoint = ring[index + 1];
// perpendicular unit vectors (outward unit normal vector):
// these indicate which direction the segments should be offset in
const unitNormalAB: Point = index === 0 ? new Point(0, 0) : point.sub(prevPoint)._unit()._perp();
const unitNormalBC: Point = index === ring.length - 1 ? new Point(0, 0) : nextPoint.sub(point)._unit()._perp();
// unit bisector direction
const bisectorDir = unitNormalAB._add(unitNormalBC)._unit();
const cosHalfAngle = bisectorDir.x * unitNormalBC.x + bisectorDir.y * unitNormalBC.y;
if (cosHalfAngle !== 0) {
bisectorDir._mult(1 / cosHalfAngle);
}
newRing.push(bisectorDir._mult(offset)._add(point));
}
newRings.push(newRing);
}
return newRings;
}
type CircleIntersectionTestParams = {
queryGeometry: Array<Point>;
size: number;
transform: IReadonlyTransform;
unwrappedTileID: UnwrappedTileID;
getElevation: undefined | ((x: number, y: number) => number);
pitchAlignment?: 'map' | 'viewport';
pitchScale?: 'map' | 'viewport';
};
function intersectionTestMapMap({queryGeometry, size}: CircleIntersectionTestParams, point: Point): boolean {
return polygonIntersectsBufferedPoint(queryGeometry, point, size);
}
function intersectionTestMapViewport({queryGeometry, size, transform, unwrappedTileID, getElevation}: CircleIntersectionTestParams, point: Point): boolean {
const w = transform.projectTileCoordinates(point.x, point.y, unwrappedTileID, getElevation).signedDistanceFromCamera;
const adjustedSize = size * (w / transform.cameraToCenterDistance);
return polygonIntersectsBufferedPoint(queryGeometry, point, adjustedSize);
}
function intersectionTestViewportMap({queryGeometry, size, transform, unwrappedTileID, getElevation}: CircleIntersectionTestParams, point: Point): boolean {
const w = transform.projectTileCoordinates(point.x, point.y, unwrappedTileID, getElevation).signedDistanceFromCamera;
const adjustedSize = size * (transform.cameraToCenterDistance / w);
return polygonIntersectsBufferedPoint(queryGeometry, projectPoint(point, transform, unwrappedTileID, getElevation), adjustedSize);
}
function intersectionTestViewportViewport({queryGeometry, size, transform, unwrappedTileID, getElevation}: CircleIntersectionTestParams, point: Point): boolean {
return polygonIntersectsBufferedPoint(queryGeometry, projectPoint(point, transform, unwrappedTileID, getElevation), size);
}
export function circleIntersection({
queryGeometry,
size,
transform,
unwrappedTileID,
getElevation,
pitchAlignment = 'map',
pitchScale = 'map'
}: CircleIntersectionTestParams, geometry): boolean {
const intersectionTest = pitchAlignment === 'map'
? (pitchScale === 'map' ? intersectionTestMapMap : intersectionTestMapViewport)
: (pitchScale === 'map' ? intersectionTestViewportMap : intersectionTestViewportViewport);
const param = {queryGeometry, size, transform, unwrappedTileID, getElevation} as CircleIntersectionTestParams;
for (const ring of geometry) {
for (const point of ring) {
if (intersectionTest(param, point)) {
return true;
}
}
}
return false;
}
function projectPoint(tilePoint: Point, transform: IReadonlyTransform, unwrappedTileID: UnwrappedTileID, getElevation: undefined | ((x: number, y: number) => number)): Point {
// Convert `tilePoint` from tile coordinates to clip coordinates.
const clipPoint = transform.projectTileCoordinates(tilePoint.x, tilePoint.y, unwrappedTileID, getElevation).point;
// Convert `clipPoint` from clip coordinates into pixel/screen coordinates.
const pixelPoint = new Point(
(clipPoint.x * 0.5 + 0.5) * transform.width,
(-clipPoint.y * 0.5 + 0.5) * transform.height
);
return pixelPoint;
}
export function projectQueryGeometry(queryGeometry: Array<Point>, transform: IReadonlyTransform, unwrappedTileID: UnwrappedTileID, getElevation: undefined | ((x: number, y: number) => number)) {
return queryGeometry.map((p) => {
return projectPoint(p, transform, unwrappedTileID, getElevation);
});
}