UNPKG

maplibre-graticule

Version:

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

268 lines (231 loc) 6.68 kB
import { getGraticule } from "./generator"; import { Map, LinePaint, GeoJSONSource } from "maplibre-gl"; export interface GraticuleConfig { minZoom?: number; maxZoom?: number; showLabels?: boolean; labelType?: "hdms" | "decimal"; labelSize?: number; labelColor?: string; longitudePosition?: "top" | "bottom"; latitudePosition?: "left" | "right"; longitudeOffset?: number[]; latitudeOffset?: number[]; paint?: LinePaint; } type graticuleJson = { meridians: GeoJSON.Feature<GeoJSON.LineString>[]; parallels: GeoJSON.Feature<GeoJSON.LineString>[]; }; export function randomString() { return Math.floor(Math.random() * 10e12).toString(36); } /** * * @param {Map} map * @returns */ function calculateResolution(map: Map) { const zoom = map.getZoom(); const container = map.getContainer(); const containerWidth = container.offsetWidth; const containerHeight = container.offsetHeight; const tileSize = 256; const metersPerPixel = (Math.PI * 2 * 6378137) / (tileSize * 2 ** zoom); const resolutionX = metersPerPixel * (containerWidth / tileSize); const resolutionY = metersPerPixel * (containerHeight / tileSize); return { x: resolutionX, y: resolutionY, }; } export class MaplibreGraticule { xid: string; yid: string; config: GraticuleConfig; updateBound: () => void; labelSize: any; map: any; constructor(config: GraticuleConfig) { this.xid = `graticule-meridains-${randomString()}`; this.yid = `graticule-parallels-${randomString()}`; this.config = config; this.updateBound = this.update.bind(this); this.labelSize = this.config.labelSize; } /** * @param {Map} map * @returns {HTMLElement} */ onAdd(map: Map): HTMLElement { this.map = map; this.map.on("load", this.updateBound); this.map.on("move", this.updateBound); if (this.map.loaded()) { this.update(); } return document.createElement("div"); } /** * @returns {void} */ onRemove(): void { if (!this.map) { return; } // Remove symbol layers const xSymbolLayer = this.map.getLayer(`symbols${this.xid}`); if (xSymbolLayer) { this.map.removeLayer(`symbols${this.xid}`); } const ySymbolLayer = this.map.getLayer(`symbols${this.yid}`); if (ySymbolLayer) { this.map.removeLayer(`symbols${this.yid}`); } const xsource = this.map.getSource(this.xid); if (xsource) { this.map.removeLayer(this.xid); this.map.removeSource(this.xid); } const ysource = this.map.getSource(this.yid); if (ysource) { this.map.removeLayer(this.yid); this.map.removeSource(this.yid); } this.map.off("load", this.updateBound); this.map.off("move", this.updateBound); this.map = undefined; } /** * @returns {void} */ update(): void { if (!this.map) { return; } const longitudePosition = this.config.longitudePosition ?? "bottom"; const latitudePosition = this.config.latitudePosition ?? "right"; const labelType = this.config.labelType ?? "hdms"; const resolution = calculateResolution(this.map); const xWidth = (resolution.x / 100) * 2; const yWidth = (resolution.y / 100) * 2; /** @type {graticuleJson} */ let graticule: graticuleJson = { meridians: [], parallels: [], }; if (this.active) { graticule = getGraticule( this.bbox, xWidth, yWidth, "kilometers", labelType, longitudePosition, latitudePosition ); } const xsource: GeoJSONSource = this.map.getSource(this.xid); if (!xsource) { this.map.addSource(this.xid, { type: "geojson", data: { type: "FeatureCollection", features: graticule.meridians }, }); this.map.addLayer({ id: this.xid, source: this.xid, type: "line", paint: this.config.paint ?? {}, }); if (this.config.showLabels) { this.map.addLayer({ id: `symbols${this.xid}`, type: "symbol", source: this.xid, layout: { "symbol-placement": "point", "text-field": "{coord}", "text-size": this.config.labelSize ?? 12, "text-anchor": longitudePosition === "top" ? "top" : "bottom", "text-offset": this.config.longitudeOffset ?? [0, 0], }, paint: { "text-color": this.config.labelColor ?? "#000000", "text-halo-blur": 1, "text-halo-color": "rgba(255,255,255,1)", "text-halo-width": 3, }, }); } } else { xsource.setData({ type: "FeatureCollection", features: graticule.meridians, }); } const ysource: GeoJSONSource = this.map.getSource(this.yid); if (!ysource) { this.map.addSource(this.yid, { type: "geojson", data: { type: "FeatureCollection", features: graticule.parallels }, }); this.map.addLayer({ id: this.yid, source: this.yid, type: "line", paint: this.config.paint ?? {}, }); if (this.config.showLabels) { this.map.addLayer({ id: `symbols${this.yid}`, type: "symbol", source: this.yid, layout: { "symbol-placement": "point", "text-field": "{coord}", "text-size": this.config.labelSize ?? 12, "text-anchor": latitudePosition === "left" ? "left" : "right", "text-offset": this.config.latitudeOffset ?? [0, 0], }, paint: { "text-color": this.config.labelColor ?? "#000000", "text-halo-blur": 1, "text-halo-color": "rgba(255,255,255,1)", "text-halo-width": 3, }, }); } } else { ysource.setData({ type: "FeatureCollection", features: graticule.parallels, }); } } /** * @returns {boolean} */ get active(): boolean { if (!this.map) { return false; } const minZoom = this.config.minZoom ?? 0; const maxZoom = this.config.maxZoom ?? 22; const zoom = this.map.getZoom(); return minZoom <= zoom && zoom < maxZoom; } /** * @returns {GeoJSON.BBox} */ get bbox(): GeoJSON.BBox { if (!this.map) { throw new Error("Invalid state"); } const bounds = this.map.getBounds(); if (bounds.getEast() - bounds.getWest() >= 360) { bounds.setNorthEast([bounds.getWest() + 360, bounds.getNorth()]); } const bbox = /** @type {GeoJSON.BBox} */ bounds.toArray().flat(); return bbox; } }