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