mapbox-gl
Version:
A WebGL interactive maps library
152 lines (119 loc) • 5.95 kB
JavaScript
// @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
};
}