maplibre-graticule
Version:
Graticuel / Grid plugin for MapLibre GL JS / Mapbox GL JS
255 lines (233 loc) • 6.62 kB
text/typescript
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,
};
}