adds maplibre print

This commit is contained in:
Christian Beutel
2024-11-29 21:41:35 +01:00
parent 390a7b52ee
commit 60c1400dfd
10 changed files with 862 additions and 332 deletions

View File

@@ -144,15 +144,15 @@ export class ElevationProfileControl implements IControl {
}
}
const waypointContainer = document.createElement("div")
waypointContainer.className = "absolute w-full"
waypointContainer.id = "waypoint-container"
this.profileContainer.append(waypointContainer);
mapContainer.appendChild(this.profileContainer);
}
const waypointContainer = document.createElement("div")
waypointContainer.className = "absolute w-full"
waypointContainer.id = "waypoint-container"
this.profileContainer.append(waypointContainer);
if (this.settings.containerClass) {
this.profileContainer.classList.add(this.settings.containerClass);
}

View File

@@ -410,6 +410,7 @@ export class ElevationProfile {
}
this.canvas = document.createElement("canvas");
this.canvas.id = "elevation-profile-chart"
appContainer.appendChild(this.canvas);
Chart.register(...registerables);
@@ -515,7 +516,7 @@ export class ElevationProfile {
tickColor: "#0002"
},
ticks: {
stepSize: 0.1,
stepSize: 1,
align: "inner",
display: this.settings.displayDistanceLabels,
color: this.settings.labelColor,

View File

@@ -0,0 +1,213 @@
import distance from '@turf/distance';
import destination from '@turf/destination';
type graticuleJson = {
meridians: GeoJSON.Feature<GeoJSON.LineString>[];
parallels: GeoJSON.Feature<GeoJSON.LineString>[];
};
import { type 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
};
}

View File

@@ -0,0 +1,266 @@
import { getGraticule } from "./generator";
import * as M 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?: any;
}
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: M.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 default 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: M.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: M.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: M.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;
}
}