UNPKG

mapbox-gl

Version:
152 lines (119 loc) 5.95 kB
// @flow import LngLat from '../lng_lat.js'; import MercatorCoordinate, {MAX_MERCATOR_LATITUDE} from '../mercator_coordinate.js'; import {mat4, mat2} from 'gl-matrix'; import {clamp, smoothstep} from '../../util/util.js'; import type Projection from './projection.js'; import type Transform from '../transform.js'; export default function getProjectionAdjustments(transform: Transform, withoutRotation?: boolean): Array<number> { const interpT = getProjectionInterpolationT(transform.projection, transform.zoom, transform.width, transform.height); const matrix = getShearAdjustment(transform.projection, transform.zoom, transform.center, interpT, withoutRotation); const scaleAdjustment = getScaleAdjustment(transform); mat4.scale(matrix, matrix, [scaleAdjustment, scaleAdjustment, 1]); return matrix; } export function getScaleAdjustment(transform: Transform): number { const projection = transform.projection; const interpT = getProjectionInterpolationT(transform.projection, transform.zoom, transform.width, transform.height); const zoomAdjustment = getZoomAdjustment(projection, transform.center); const zoomAdjustmentOrigin = getZoomAdjustment(projection, LngLat.convert(projection.center)); const scaleAdjustment = Math.pow(2, zoomAdjustment * interpT + (1 - interpT) * zoomAdjustmentOrigin); return scaleAdjustment; } export function getProjectionAdjustmentInverted(transform: Transform): Array<number> { const m = getProjectionAdjustments(transform, true); return mat2.invert([], [ m[0], m[1], m[4], m[5]]); } export function getProjectionInterpolationT(projection: Projection, zoom: number, width: number, height: number, maxSize: number = Infinity): number { const range = projection.range; if (!range) return 0; const size = Math.min(maxSize, Math.max(width, height)); // The interpolation ranges are manually defined based on what makes // sense in a 1024px wide map. Adjust the ranges to the current size // of the map. The smaller the map, the earlier you can start unskewing. const rangeAdjustment = Math.log(size / 1024) / Math.LN2; const zoomA = range[0] + rangeAdjustment; const zoomB = range[1] + rangeAdjustment; const t = smoothstep(zoomA, zoomB, zoom); return t; } // approx. kilometers per longitude degree at equator const offset = 1 / 40000; /* * Calculates the scale difference between Mercator and the given projection at a certain location. */ function getZoomAdjustment(projection: Projection, loc: LngLat) { // make sure we operate within mercator space for adjustments (they can go over for other projections) const lat = clamp(loc.lat, -MAX_MERCATOR_LATITUDE, MAX_MERCATOR_LATITUDE); const loc1 = new LngLat(loc.lng - 180 * offset, lat); const loc2 = new LngLat(loc.lng + 180 * offset, lat); const p1 = projection.project(loc1.lng, lat); const p2 = projection.project(loc2.lng, lat); const m1 = MercatorCoordinate.fromLngLat(loc1); const m2 = MercatorCoordinate.fromLngLat(loc2); const pdx = p2.x - p1.x; const pdy = p2.y - p1.y; const mdx = m2.x - m1.x; const mdy = m2.y - m1.y; const scale = Math.sqrt((mdx * mdx + mdy * mdy) / (pdx * pdx + pdy * pdy)); return Math.log(scale) / Math.LN2; } function getShearAdjustment(projection: Projection, zoom: number, loc: LngLat, interpT: number, withoutRotation?: boolean) { // create two locations a tiny amount (~1km) east and west of the given location const locw = new LngLat(loc.lng - 180 * offset, loc.lat); const loce = new LngLat(loc.lng + 180 * offset, loc.lat); const pw = projection.project(locw.lng, locw.lat); const pe = projection.project(loce.lng, loce.lat); const pdx = pe.x - pw.x; const pdy = pe.y - pw.y; // Calculate how much the map would need to be rotated to make east-west in // projected coordinates be left-right const angleAdjust = -Math.atan2(pdy, pdx); // Pick a location identical to the original one except for poles to make sure we're within mercator bounds const mc2 = MercatorCoordinate.fromLngLat(loc); mc2.y = clamp(mc2.y, -1 + offset, 1 - offset); const loc2 = mc2.toLngLat(); const p2 = projection.project(loc2.lng, loc2.lat); // Find the projected coordinates of two locations, one slightly south and one slightly east. // Then calculate the transform that would make the projected coordinates of the two locations be: // - equal distances from the original location // - perpendicular to one another // // Only the position of the coordinate to the north is adjusted. // The coordinate to the east stays where it is. const mc3 = MercatorCoordinate.fromLngLat(loc2); mc3.x += offset; const loc3 = mc3.toLngLat(); const p3 = projection.project(loc3.lng, loc3.lat); const pdx3 = p3.x - p2.x; const pdy3 = p3.y - p2.y; const delta3 = rotate(pdx3, pdy3, angleAdjust); const mc4 = MercatorCoordinate.fromLngLat(loc2); mc4.y += offset; const loc4 = mc4.toLngLat(); const p4 = projection.project(loc4.lng, loc4.lat); const pdx4 = p4.x - p2.x; const pdy4 = p4.y - p2.y; const delta4 = rotate(pdx4, pdy4, angleAdjust); const scale = Math.abs(delta3.x) / Math.abs(delta4.y); const unrotate = mat4.identity([]); mat4.rotateZ(unrotate, unrotate, (-angleAdjust) * (1 - (withoutRotation ? 0 : interpT))); // unskew const shear = mat4.identity([]); mat4.scale(shear, shear, [1, 1 - (1 - scale) * interpT, 1]); shear[4] = -delta4.x / delta4.y * interpT; // unrotate mat4.rotateZ(shear, shear, angleAdjust); mat4.multiply(shear, unrotate, shear); return shear; } function rotate(x: number, y: number, angle: number) { const cos = Math.cos(angle); const sin = Math.sin(angle); return { x: x * cos - y * sin, y: x * sin + y * cos }; }