export interface LonLatPoint { lon: number lat: number } export interface WebMercatorPoint { x: number y: number } export interface WorldTilePoint { x: number y: number } export interface MapCalibration { offsetEastMeters: number offsetNorthMeters: number rotationDeg: number scale: number } const MAX_LATITUDE = 85.05112878 const EARTH_RADIUS = 6378137 function clampLatitude(lat: number): number { return Math.max(-MAX_LATITUDE, Math.min(MAX_LATITUDE, lat)) } export function lonLatToWebMercator(point: LonLatPoint): WebMercatorPoint { const latitude = clampLatitude(point.lat) const lonRad = point.lon * Math.PI / 180 const latRad = latitude * Math.PI / 180 return { x: EARTH_RADIUS * lonRad, y: EARTH_RADIUS * Math.log(Math.tan(Math.PI / 4 + latRad / 2)), } } export function webMercatorToLonLat(point: WebMercatorPoint): LonLatPoint { return { lon: point.x / EARTH_RADIUS * 180 / Math.PI, lat: (2 * Math.atan(Math.exp(point.y / EARTH_RADIUS)) - Math.PI / 2) * 180 / Math.PI, } } export function lonLatToWorldTile(point: LonLatPoint, zoom: number): WorldTilePoint { const latitude = clampLatitude(point.lat) const scale = Math.pow(2, zoom) const latRad = latitude * Math.PI / 180 return { x: (point.lon + 180) / 360 * scale, y: (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * scale, } } export function worldTileToLonLat(point: WorldTilePoint, zoom: number): LonLatPoint { const scale = Math.pow(2, zoom) const lon = point.x / scale * 360 - 180 const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * point.y / scale))) return { lon, lat: latRad * 180 / Math.PI, } } export function applyMapCalibration(point: LonLatPoint, calibration: MapCalibration, origin: LonLatPoint): LonLatPoint { const scale = calibration.scale || 1 const rotationDeg = calibration.rotationDeg || 0 const offsetEastMeters = calibration.offsetEastMeters || 0 const offsetNorthMeters = calibration.offsetNorthMeters || 0 if ( Math.abs(scale - 1) < 0.000001 && Math.abs(rotationDeg) < 0.000001 && Math.abs(offsetEastMeters) < 0.000001 && Math.abs(offsetNorthMeters) < 0.000001 ) { return point } const originMercator = lonLatToWebMercator(origin) const pointMercator = lonLatToWebMercator(point) const deltaX = pointMercator.x - originMercator.x const deltaY = pointMercator.y - originMercator.y const scaledX = deltaX * scale const scaledY = deltaY * scale const rotationRad = rotationDeg * Math.PI / 180 const cos = Math.cos(rotationRad) const sin = Math.sin(rotationRad) const calibratedMercator: WebMercatorPoint = { x: originMercator.x + scaledX * cos - scaledY * sin + offsetEastMeters, y: originMercator.y + scaledX * sin + scaledY * cos + offsetNorthMeters, } return webMercatorToLonLat(calibratedMercator) } export function calibratedLonLatToWorldTile(point: LonLatPoint, zoom: number, calibration: MapCalibration, origin: LonLatPoint): WorldTilePoint { return lonLatToWorldTile(applyMapCalibration(point, calibration, origin), zoom) } const CHINA_AXIS = 6378245 const CHINA_EE = 0.00669342162296594323 function isOutsideChina(point: LonLatPoint): boolean { return point.lon < 72.004 || point.lon > 137.8347 || point.lat < 0.8293 || point.lat > 55.8271 } function transformLat(x: number, y: number): number { let result = -100 + 2 * x + 3 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)) result += (20 * Math.sin(6 * x * Math.PI) + 20 * Math.sin(2 * x * Math.PI)) * 2 / 3 result += (20 * Math.sin(y * Math.PI) + 40 * Math.sin(y / 3 * Math.PI)) * 2 / 3 result += (160 * Math.sin(y / 12 * Math.PI) + 320 * Math.sin(y * Math.PI / 30)) * 2 / 3 return result } function transformLon(x: number, y: number): number { let result = 300 + x + 2 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)) result += (20 * Math.sin(6 * x * Math.PI) + 20 * Math.sin(2 * x * Math.PI)) * 2 / 3 result += (20 * Math.sin(x * Math.PI) + 40 * Math.sin(x / 3 * Math.PI)) * 2 / 3 result += (150 * Math.sin(x / 12 * Math.PI) + 300 * Math.sin(x / 30 * Math.PI)) * 2 / 3 return result } export function gcj02ToWgs84(point: LonLatPoint): LonLatPoint { if (isOutsideChina(point)) { return point } const dLat = transformLat(point.lon - 105, point.lat - 35) const dLon = transformLon(point.lon - 105, point.lat - 35) const radLat = point.lat / 180 * Math.PI const magic = Math.sin(radLat) const sqrtMagic = Math.sqrt(1 - CHINA_EE * magic * magic) const latOffset = (dLat * 180) / ((CHINA_AXIS * (1 - CHINA_EE)) / (sqrtMagic * sqrtMagic * sqrtMagic) * Math.PI) const lonOffset = (dLon * 180) / (CHINA_AXIS / sqrtMagic * Math.cos(radLat) * Math.PI) return { lon: point.lon - lonOffset, lat: point.lat - latOffset, } }