UNPKG

maplibre-graticule

Version:

Graticuel / Grid plugin for MapLibre GL JS / Mapbox GL JS

255 lines (233 loc) 6.62 kB
import distance from "@turf/distance"; import destination from "@turf/destination"; type graticuleJson = { meridians: GeoJSON.Feature<GeoJSON.LineString>[]; parallels: GeoJSON.Feature<GeoJSON.LineString>[]; }; import { Units } from "@turf/helpers"; /** * * @param {number} n * @param {number} decimals * @returns {number} */ export function toFixed(n: number, decimals: number): number { const factor = Math.pow(10, decimals); return Math.round(n * factor) / factor; } /** * * @param {number} a * @param {number} b * @returns {number} */ export function modulo(a: number, b: number): number { const r = a % b; return r * b < 0 ? r + b : r; } /** * * @param {number} number * @param {number} width * @param {number | undefined} precision * @returns {string} */ export function padNumber( number: number, width: number, precision: number | undefined = undefined ): string { const numberString = precision !== undefined ? number.toFixed(precision) : "" + number; let decimal = numberString.indexOf("."); decimal = decimal === -1 ? numberString.length : decimal; return decimal > width ? numberString : new Array(1 + width - decimal).join("0") + numberString; } /** * * @param {string} hemispheres * @param {number} degrees * @param {number} fractionDigits * @returns {string} */ export function degreesToStringHDMS( hemispheres: string, degrees: number, fractionDigits: number ): string { const normalizedDegrees = modulo(degrees + 180, 360) - 180; const x = Math.abs(3600 * normalizedDegrees); const decimals = fractionDigits || 0; let deg = Math.floor(x / 3600); let min = Math.floor((x - deg * 3600) / 60); let sec = toFixed(x - deg * 3600 - min * 60, decimals); if (sec >= 60) { sec = 0; min += 1; } if (min >= 60) { min = 0; deg += 1; } let hdms = deg + "\u00b0"; if (min !== 0 || sec !== 0) { hdms += " " + padNumber(min, 2) + "\u2032"; } if (sec !== 0) { hdms += " " + padNumber(sec, 2, decimals) + "\u2033"; } if (normalizedDegrees !== 0) { hdms += " " + hemispheres; } return hdms; } /** * @param {GeoJSON.BBox} bbox * @param {number} graticuleWidth * @param {number} graticuleHeight * @param {Units} units * @param {string} labelType * @param {string} longitudePosition * @param {string} latitudePosition * @returns {graticuleJson} */ export function getGraticule( bbox: GeoJSON.BBox, graticuleWidth: number, graticuleHeight: number, units: Units, labelType: string, longitudePosition: string, latitudePosition: string ): graticuleJson { const earthCircumference = Math.ceil( distance([0, 0], [180, 0], { units }) * 2 ); const maxColumns = Math.floor(earthCircumference / graticuleWidth); const fullDistance: ( from: GeoJSON.Position, to: GeoJSON.Position, options: { units: Units } ) => number = (from, to, options): number => { const dist = distance(from, to, options); if (Math.abs(to[0] - from[0]) >= 180) { return earthCircumference - dist; } return dist; }; const meridians: GeoJSON.Feature<GeoJSON.LineString>[] = []; const parallels = []; const west = bbox[0]; const south = bbox[1]; const east = bbox[2]; const north = bbox[3]; // calculate graticule start point const deltaX = (west < 0 ? -1 : 1) * fullDistance([0, 0], [west, 0], { units }); const deltaY = (south < 0 ? -1 : 1) * fullDistance([0, 0], [0, south], { units }); const startDeltaX = Math.ceil(deltaX / graticuleWidth) * graticuleWidth; const startDeltaY = Math.ceil(deltaY / graticuleHeight) * graticuleHeight; const startPoint: GeoJSON.Position = [ destination([0, 0], startDeltaX, 90, { units }).geometry.coordinates[0], destination([0, 0], startDeltaY, 0, { units }).geometry.coordinates[1], ]; // calculate graticule columns and rows count const width = fullDistance([west, 0], [east, 0], { units }); const height = fullDistance([0, south], [0, north], { units }); const columns = Math.min(Math.ceil(width / graticuleWidth), maxColumns); const rows = Math.ceil(height / graticuleHeight); let currentPoint: GeoJSON.Position; // meridians currentPoint = startPoint; for (let i = 0; i < columns; i++) { let coordinates: GeoJSON.Position[]; if (longitudePosition === "bottom") { coordinates = [ [currentPoint[0], south], [currentPoint[0], north], ]; } else { coordinates = [ [currentPoint[0], north], [currentPoint[0], south], ]; } let hemisphere; if (currentPoint[0] > 0) { hemisphere = "E"; } else { hemisphere = "W"; } const hdms = degreesToStringHDMS(hemisphere, currentPoint[0], 2); let feature: GeoJSON.Feature<GeoJSON.LineString>; if (labelType === "hdms") { feature = { type: "Feature", geometry: { type: "LineString", coordinates }, properties: { coord: hdms }, }; } else { feature = { type: "Feature", geometry: { type: "LineString", coordinates }, properties: { coord: currentPoint[0].toFixed(3) + "°" }, }; } meridians.push(feature); currentPoint = [ destination([currentPoint[0], 0], graticuleWidth, 90, { units }).geometry .coordinates[0], currentPoint[1], ]; } // parallels currentPoint = startPoint; for (let i = 0; i < rows; i++) { let coordinates: GeoJSON.Position[]; if (latitudePosition === "right") { coordinates = [ [east, currentPoint[1]], [west, currentPoint[1]], ]; } else { coordinates = [ [west, currentPoint[1]], [east, currentPoint[1]], ]; } let hemisphere = "S"; if (currentPoint[1] > 0) { hemisphere = "N"; } else { hemisphere = "S"; } const hdms = degreesToStringHDMS(hemisphere, currentPoint[1], 2); let feature: GeoJSON.Feature<GeoJSON.LineString>; if (labelType === "hdms") { feature = { type: "Feature", geometry: { type: "LineString", coordinates }, properties: { coord: hdms }, }; } else { feature = { type: "Feature", geometry: { type: "LineString", coordinates }, properties: { coord: currentPoint[1].toFixed(3) + "°" }, }; } parallels.push(feature); currentPoint = [ currentPoint[0], destination([0, currentPoint[1]], graticuleHeight, 0, { units }).geometry .coordinates[1], ]; } return { meridians: meridians, parallels: parallels, }; }