adds maplibre print
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
213
web/src/lib/vendor/maplibre-graticule/generator.ts
vendored
Normal file
213
web/src/lib/vendor/maplibre-graticule/generator.ts
vendored
Normal 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
|
||||
};
|
||||
}
|
||||
266
web/src/lib/vendor/maplibre-graticule/maplibre-graticule.ts
vendored
Normal file
266
web/src/lib/vendor/maplibre-graticule/maplibre-graticule.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user