UNPKG

@deck.gl/core

Version:

deck.gl core library

323 lines (285 loc) 9.83 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors // View and Projection Matrix calculations for mapbox-js style // map view properties import Viewport from './viewport'; import { pixelsToWorld, getViewMatrix, addMetersToLngLat, unitsPerMeter, getProjectionParameters, altitudeToFovy, fovyToAltitude, fitBounds, getBounds } from '@math.gl/web-mercator'; import {Padding} from './viewport'; import {Matrix4, clamp, vec2} from '@math.gl/core'; export type WebMercatorViewportOptions = { /** Name of the viewport */ id?: string; /** Left offset from the canvas edge, in pixels */ x?: number; /** Top offset from the canvas edge, in pixels */ y?: number; /** Viewport width in pixels */ width?: number; /** Viewport height in pixels */ height?: number; /** Longitude in degrees */ longitude?: number; /** Latitude in degrees */ latitude?: number; /** Tilt of the camera in degrees */ pitch?: number; /** Heading of the camera in degrees */ bearing?: number; /** Camera altitude relative to the viewport height, legacy property used to control the FOV. Default `1.5` */ altitude?: number; /** Camera fovy in degrees. If provided, overrides `altitude` */ fovy?: number; /** Viewport center in world space. If geospatial, refers to meter offsets from lng, lat, elevation */ position?: number[]; /** Zoom level */ zoom?: number; /** Padding around the viewport, in pixels. */ padding?: Padding | null; /** Model matrix of viewport center */ modelMatrix?: number[] | null; /** Custom projection matrix */ projectionMatrix?: number[]; /** Use orthographic projection */ orthographic?: boolean; /** Scaler for the near plane, 1 unit equals to the height of the viewport. Default `0.1` */ nearZMultiplier?: number; /** Scaler for the far plane, 1 unit equals to the distance from the camera to the edge of the screen. Default `1.01` */ farZMultiplier?: number; /** Optionally override the near plane position. `nearZMultiplier` is ignored if `nearZ` is supplied. */ nearZ?: number; /** Optionally override the far plane position. `farZMultiplier` is ignored if `farZ` is supplied. */ farZ?: number; /** Render multiple copies of the world */ repeat?: boolean; /** Internal use */ worldOffset?: number; /** @deprecated Revert to approximated meter size calculation prior to v8.5 */ legacyMeterSizes?: boolean; }; /** * Manages transformations to/from WGS84 coordinates using the Web Mercator Projection. */ export default class WebMercatorViewport extends Viewport { static displayName = 'WebMercatorViewport'; longitude: number; latitude: number; pitch: number; bearing: number; altitude: number; fovy: number; orthographic: boolean; /** Each sub viewport renders one copy of the world if repeat:true. The list is generated and cached on first request. */ private _subViewports: WebMercatorViewport[] | null; /** @deprecated Revert to approximated meter size calculation prior to v8.5 */ private _pseudoMeters: boolean; /* eslint-disable complexity, max-statements */ constructor(opts: WebMercatorViewportOptions = {}) { const { latitude = 0, longitude = 0, zoom = 0, pitch = 0, bearing = 0, nearZMultiplier = 0.1, farZMultiplier = 1.01, nearZ, farZ, orthographic = false, projectionMatrix, repeat = false, worldOffset = 0, position, padding, // backward compatibility // TODO: remove in v9 legacyMeterSizes = false } = opts; let {width, height, altitude = 1.5} = opts; const scale = Math.pow(2, zoom); // Silently allow apps to send in 0,0 to facilitate isomorphic render etc width = width || 1; height = height || 1; let fovy; let projectionParameters: any = null; if (projectionMatrix) { altitude = projectionMatrix[5] / 2; fovy = altitudeToFovy(altitude); } else { if (opts.fovy) { fovy = opts.fovy; altitude = fovyToAltitude(fovy); } else { fovy = altitudeToFovy(altitude); } let offset: [number, number] | undefined; if (padding) { const {top = 0, bottom = 0} = padding; offset = [0, clamp((top + height - bottom) / 2, 0, height) - height / 2]; } projectionParameters = getProjectionParameters({ width, height, scale, center: position && [0, 0, position[2] * unitsPerMeter(latitude)], offset, pitch, fovy, nearZMultiplier, farZMultiplier }); if (Number.isFinite(nearZ)) { projectionParameters.near = nearZ; } if (Number.isFinite(farZ)) { projectionParameters.far = farZ; } } // The uncentered matrix allows us two move the center addition to the // shader (cheap) which gives a coordinate system that has its center in // the layer's center position. This makes rotations and other modelMatrx // transforms much more useful. let viewMatrixUncentered = getViewMatrix({ height, pitch, bearing, scale, altitude }); if (worldOffset) { const viewOffset = new Matrix4().translate([512 * worldOffset, 0, 0]); viewMatrixUncentered = viewOffset.multiplyLeft(viewMatrixUncentered); } super({ ...opts, // x, y, width, height, // view matrix viewMatrix: viewMatrixUncentered, longitude, latitude, zoom, // projection matrix parameters ...projectionParameters, fovy, focalDistance: altitude }); // Save parameters this.latitude = latitude; this.longitude = longitude; this.zoom = zoom; this.pitch = pitch; this.bearing = bearing; this.altitude = altitude; this.fovy = fovy; this.orthographic = orthographic; this._subViewports = repeat ? [] : null; this._pseudoMeters = legacyMeterSizes; Object.freeze(this); } /* eslint-enable complexity, max-statements */ get subViewports(): WebMercatorViewport[] | null { if (this._subViewports && !this._subViewports.length) { // Cache sub viewports so that we only calculate them once const bounds = this.getBounds(); const minOffset = Math.floor((bounds[0] + 180) / 360); const maxOffset = Math.ceil((bounds[2] - 180) / 360); for (let x = minOffset; x <= maxOffset; x++) { const offsetViewport = x ? new WebMercatorViewport({ ...this, worldOffset: x }) : this; this._subViewports.push(offsetViewport); } } return this._subViewports; } projectPosition(xyz: number[]): [number, number, number] { if (this._pseudoMeters) { // Backward compatibility return super.projectPosition(xyz); } const [X, Y] = this.projectFlat(xyz); const Z = (xyz[2] || 0) * unitsPerMeter(xyz[1]); return [X, Y, Z]; } unprojectPosition(xyz: number[]): [number, number, number] { if (this._pseudoMeters) { // Backward compatibility return super.unprojectPosition(xyz); } const [X, Y] = this.unprojectFlat(xyz); const Z = (xyz[2] || 0) / unitsPerMeter(Y); return [X, Y, Z]; } /** * Add a meter delta to a base lnglat coordinate, returning a new lnglat array * * Note: Uses simple linear approximation around the viewport center * Error increases with size of offset (roughly 1% per 100km) * * @param {[Number,Number]|[Number,Number,Number]) lngLatZ - base coordinate * @param {[Number,Number]|[Number,Number,Number]) xyz - array of meter deltas * @return {[Number,Number]|[Number,Number,Number]) array of [lng,lat,z] deltas */ addMetersToLngLat(lngLatZ: number[], xyz: number[]): number[] { return addMetersToLngLat(lngLatZ, xyz); } panByPosition(coords: number[], pixel: number[]): WebMercatorViewportOptions { const fromLocation = pixelsToWorld(pixel, this.pixelUnprojectionMatrix); const toLocation = this.projectFlat(coords); const translate = vec2.add([], toLocation, vec2.negate([], fromLocation)); const newCenter = vec2.add([], this.center, translate); const [longitude, latitude] = this.unprojectFlat(newCenter); return {longitude, latitude}; } getBounds(options: {z?: number} = {}): [number, number, number, number] { // @ts-ignore const corners = getBounds(this, options.z || 0); return [ Math.min(corners[0][0], corners[1][0], corners[2][0], corners[3][0]), Math.min(corners[0][1], corners[1][1], corners[2][1], corners[3][1]), Math.max(corners[0][0], corners[1][0], corners[2][0], corners[3][0]), Math.max(corners[0][1], corners[1][1], corners[2][1], corners[3][1]) ]; } /** * Returns a new viewport that fit around the given rectangle. * Only supports non-perspective mode. */ fitBounds( /** [[lon, lat], [lon, lat]] */ bounds: [[number, number], [number, number]], options: { /** If not supplied, will use the current width of the viewport (default `1`) */ width?: number; /** If not supplied, will use the current height of the viewport (default `1`) */ height?: number; /** In degrees, 0.01 would be about 1000 meters */ minExtent?: number; /** Max zoom level */ maxZoom?: number; /** Extra padding in pixels */ padding?: number | Required<Padding>; /** Center shift in pixels */ offset?: number[]; } = {} ) { const {width, height} = this; const {longitude, latitude, zoom} = fitBounds({width, height, bounds, ...options}); return new WebMercatorViewport({width, height, longitude, latitude, zoom}); } }