UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

252 lines (232 loc) • 13 kB
import {mat4} from 'gl-matrix'; import {EXTENT} from '../../data/extent.ts'; import {clamp, degreesToRadians, MAX_VALID_LATITUDE, zoomScale, type Mat4f64} from '../../util/util.ts'; import {MercatorCoordinate} from '../mercator_coordinate.ts'; import Point from '@mapbox/point-geometry'; import type {WorldCoordinateHelper} from '../transform_interface.ts'; import type {UnwrappedTileIDType} from '../transform_helper.ts'; import type {LngLat} from '../lng_lat.ts'; /* * The maximum angle to use for the Mercator horizon. This must be less than 90 * to prevent errors in `MercatorTransform::_calcMatrices()`. It shouldn't be too close * to 90, or the distance to the horizon will become very large, unnecessarily increasing * the number of tiles needed to render the map. */ export const maxMercatorHorizonAngle = 89.25; /** * Returns mercator coordinates in range 0..1 for given coordinates inside a specified tile. * @param inTileX - X coordinate in tile units - range [0..EXTENT]. * @param inTileY - Y coordinate in tile units - range [0..EXTENT]. * @param canonicalTileID - Tile canonical ID - mercator X, Y and zoom. * @returns Mercator coordinates of the specified point in range [0..1]. */ export function tileCoordinatesToMercatorCoordinates(inTileX: number, inTileY: number, canonicalTileID: {x: number; y: number; z: number}): MercatorCoordinate { const scale = 1.0 / (1 << canonicalTileID.z); return new MercatorCoordinate( inTileX / EXTENT * scale + canonicalTileID.x * scale, inTileY / EXTENT * scale + canonicalTileID.y * scale ); } /** * Returns LngLat for given in-tile coordinates and tile ID. * @param inTileX - X coordinate in tile units - range [0..EXTENT]. * @param inTileY - Y coordinate in tile units - range [0..EXTENT]. * @param canonicalTileID - Tile canonical ID - mercator X, Y and zoom. */ export function tileCoordinatesToLocation(inTileX: number, inTileY: number, canonicalTileID: {x: number; y: number; z: number}): LngLat { return tileCoordinatesToMercatorCoordinates(inTileX, inTileY, canonicalTileID).toLngLat(); } /** * Convert from LngLat to world coordinates (the projection's 0..1 world square scaled by world size). * @param worldSize - World size computed from zoom level and tile size. * @param lnglat - The location to convert. * @param helper - The lng/lat to world mapping; latitude is clamped to the valid mercator range only for a wrapping (mercator) helper. * @returns Point */ export function projectToWorldCoordinates(worldSize: number, lnglat: LngLat, helper: WorldCoordinateHelper): Point { const lat = helper.wraps ? clamp(lnglat.lat, -MAX_VALID_LATITUDE, MAX_VALID_LATITUDE) : lnglat.lat; const {x, y} = helper.worldFromLngLat(lnglat.lng, lat); return new Point(x * worldSize, y * worldSize); } /** * Convert from world coordinates (the projection's 0..1 world square scaled by world size) to LngLat. * @param worldSize - World size computed from zoom level and tile size. * @param point - World coordinate. * @param helper - The lng/lat to world mapping. * @returns LngLat */ export function unprojectFromWorldCoordinates(worldSize: number, point: Point, helper: WorldCoordinateHelper): LngLat { return helper.lngLatFromWorld(point.x / worldSize, point.y / worldSize); } /** * Calculate pixel height of the visible horizon in relation to map-center (e.g. height/2), * multiplied by a static factor to simulate the earth-radius. * The calculated value is the horizontal line from the camera-height to sea-level. * @returns Horizon above center in pixels. */ export function getMercatorHorizon(transform: {pitch: number; cameraToCenterDistance: number}): number { return transform.cameraToCenterDistance * Math.min(Math.tan(degreesToRadians(90 - transform.pitch)) * 0.85, Math.tan(degreesToRadians(maxMercatorHorizonAngle - transform.pitch))); } export function calculateTileMatrix(unwrappedTileID: UnwrappedTileIDType, worldSize: number): Mat4f64 { const canonical = unwrappedTileID.canonical; const scale = worldSize / zoomScale(canonical.z); const unwrappedX = canonical.x + Math.pow(2, canonical.z) * unwrappedTileID.wrap; const worldMatrix: Mat4f64 = new Float64Array(16); mat4.identity(worldMatrix); mat4.translate(worldMatrix, worldMatrix, [unwrappedX * scale, canonical.y * scale, 0]); mat4.scale(worldMatrix, worldMatrix, [scale / EXTENT, scale / EXTENT, 1]); return worldMatrix; } /** * Returns the camera position for a center already mapped to world coordinates. * Callers resolve the center through the transform's `WorldCoordinateHelper`; keeping the helper * out of this function lets the engine inline it on the per-frame camera path. * @param centerMercator - the center in world coordinates, with its elevation in `z` * @param dMercator - camera to center distance in world units */ export function cameraMercatorCoordinateFromCenterAndRotation(centerMercator: MercatorCoordinate, pitch: number, bearing: number, dMercator: number): MercatorCoordinate { const {x, y, z} = cameraDirectionFromPitchBearing(pitch, bearing); const dxMercator = dMercator * -x; const dyMercator = dMercator * -y; // Unlike x and y, z already points from the center up towards the camera. const dzMercator = dMercator * z; return new MercatorCoordinate(centerMercator.x + dxMercator, centerMercator.y + dyMercator, centerMercator.z + dzMercator); } /** * Returns the position of the camera in mercator coordinates, with its altitude in `z`. * Computed from the center, pitch, bearing and camera distance, so it holds for any projection. * @param center - The center to compute from, the transform's own by default. */ export function cameraMercatorCoordinate(transform: { center: LngLat; elevation: number; pitch: number; bearing: number; cameraToCenterDistance: number; worldSize: number; worldCoordinateHelper: WorldCoordinateHelper; }, center: LngLat = transform.center): MercatorCoordinate { const worldCoordinateHelper = transform.worldCoordinateHelper; const mercUnitsPerMeter = worldCoordinateHelper.worldZFromAltitude(1, center); const pixelPerMeter = mercUnitsPerMeter * transform.worldSize; const distance = transform.cameraToCenterDistance / pixelPerMeter; const centerMercator = worldCoordinateHelper.worldFromLngLat(center.lng, center.lat, transform.elevation); return cameraMercatorCoordinateFromCenterAndRotation(centerMercator, transform.pitch, transform.bearing, distance * mercUnitsPerMeter); } export function cameraDirectionFromPitchBearing(pitch: number, bearing: number): {x: number; y: number; z: number} { const pitchRadians = degreesToRadians(pitch); const bearingRadians = degreesToRadians(bearing); const z = Math.cos(-pitchRadians); const h = Math.sin(pitchRadians); const x = h * Math.sin(bearingRadians); const y = -h * Math.cos(bearingRadians); return {x, y, z}; } /** * The number of steps a lng/lat box is cut into along each axis by {@link lngLatBoxToWorldSamples}. */ const BOX_SAMPLE_STEPS = 16; /** * Projects a lng/lat box into the world square as a grid of points, {@link BOX_SAMPLE_STEPS} + 1 on a side, * edges and corners included. The corners alone miss most of a box whose edges curve, as a parallel does * around a pole, and the inside of the box matters where the projection runs off to infinity, as transverse * mercator does on the equator 90 degrees from its central meridian. * `bulge` is the farthest an edge strays from the line between two neighboring points, measured halfway * between them, which bounds how far an edge reaches past the points. It is 0 where every edge projects to * a straight line, as in mercator. */ export function lngLatBoxToWorldSamples(worldCoordinateHelper: WorldCoordinateHelper, west: number, south: number, east: number, north: number): {points: Point[]; bulge: number} { const toPoint = (lng: number, lat: number) => { const {x, y} = worldCoordinateHelper.worldFromLngLat(lng, lat); return new Point(x, y); }; const lngAt = (step: number) => step === BOX_SAMPLE_STEPS ? east : west + (east - west) * step / BOX_SAMPLE_STEPS; const latAt = (step: number) => step === BOX_SAMPLE_STEPS ? south : north + (south - north) * step / BOX_SAMPLE_STEPS; const points: Point[] = []; for (let row = 0; row <= BOX_SAMPLE_STEPS; row++) { for (let column = 0; column <= BOX_SAMPLE_STEPS; column++) { points.push(toPoint(lngAt(column), latAt(row))); } } const pointAt = (row: number, column: number) => points[row * (BOX_SAMPLE_STEPS + 1) + column]; let bulge = 0; for (let step = 0; step < BOX_SAMPLE_STEPS; step++) { const lngHalfway = (lngAt(step) + lngAt(step + 1)) / 2; const latHalfway = (latAt(step) + latAt(step + 1)) / 2; const edges: Array<[Point, Point, Point]> = [ [pointAt(0, step), pointAt(0, step + 1), toPoint(lngHalfway, north)], [pointAt(BOX_SAMPLE_STEPS, step), pointAt(BOX_SAMPLE_STEPS, step + 1), toPoint(lngHalfway, south)], [pointAt(step, 0), pointAt(step + 1, 0), toPoint(west, latHalfway)], [pointAt(step, BOX_SAMPLE_STEPS), pointAt(step + 1, BOX_SAMPLE_STEPS), toPoint(east, latHalfway)], ]; for (const [start, end, halfway] of edges) { const chord = end.sub(start); const toHalfway = halfway.sub(start); const chordLength = chord.mag(); const distance = chordLength > 0 ? Math.abs(chord.x * toHalfway.y - chord.y * toHalfway.x) / chordLength : toHalfway.mag(); if (distance > bulge) bulge = distance; } } return {points, bulge}; } /** * Projects a lng/lat box into the world square and returns the axis-aligned rectangle that contains it: * the rectangle around {@link lngLatBoxToWorldSamples}, grown by their bulge. In mercator that is exactly * the rectangle of the projected corners. */ export function lngLatBoxToWorldBox(worldCoordinateHelper: WorldCoordinateHelper, west: number, south: number, east: number, north: number): {minX: number; minY: number; maxX: number; maxY: number} { const {points, bulge} = lngLatBoxToWorldSamples(worldCoordinateHelper, west, south, east, north); let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (const {x, y} of points) { if (x < minX) minX = x; if (y < minY) minY = y; if (x > maxX) maxX = x; if (y > maxY) maxY = y; } return {minX: minX - bulge, minY: minY - bulge, maxX: maxX + bulge, maxY: maxY + bulge}; } /** * Maps the four corners of a world rectangle back to lng/lat and returns the box that contains them. * That is exact in mercator. Where `x` and `y` both depend on `lng` and `lat` it can miss a curved edge * or a pole inside the rectangle; its one caller is the GeoJSON reload check, which a GeoJSON source skips * in a registered projection by reloading every tile on a diff. */ export function worldBoxToLngLatBox(worldCoordinateHelper: WorldCoordinateHelper, minX: number, minY: number, maxX: number, maxY: number): {west: number; south: number; east: number; north: number} { const corners = [ worldCoordinateHelper.lngLatFromWorld(minX, minY), worldCoordinateHelper.lngLatFromWorld(maxX, minY), worldCoordinateHelper.lngLatFromWorld(maxX, maxY), worldCoordinateHelper.lngLatFromWorld(minX, maxY), ]; return { west: Math.min(corners[0].lng, corners[1].lng, corners[2].lng, corners[3].lng), south: Math.min(corners[0].lat, corners[1].lat, corners[2].lat, corners[3].lat), east: Math.max(corners[0].lng, corners[1].lng, corners[2].lng, corners[3].lng), north: Math.max(corners[0].lat, corners[1].lat, corners[2].lat, corners[3].lat), }; } /** * The step in degrees over which {@link degreesPerWorldUnit} measures the size of a degree. */ const DEGREE_STEP = 1e-4; /** * Returns how many degrees one world unit spans at a location. A world that wraps is 360 degrees wide. * Any other world is measured at the location along longitude and along latitude, and the direction * with the fewest degrees counts. */ export function degreesPerWorldUnit(worldCoordinateHelper: WorldCoordinateHelper, lngLat: LngLat): number { if (worldCoordinateHelper.wraps) return 360; const latStep = lngLat.lat > 0 ? -DEGREE_STEP : DEGREE_STEP; const world = worldCoordinateHelper.worldFromLngLat(lngLat.lng, lngLat.lat); const alongLng = worldCoordinateHelper.worldFromLngLat(lngLat.lng + DEGREE_STEP, lngLat.lat); const alongLat = worldCoordinateHelper.worldFromLngLat(lngLat.lng, lngLat.lat + latStep); const worldUnits = Math.max( Math.hypot(alongLng.x - world.x, alongLng.y - world.y), Math.hypot(alongLat.x - world.x, alongLat.y - world.y) ); return DEGREE_STEP / worldUnits; }