adds maplibre print
This commit is contained in:
55
web/package-lock.json
generated
55
web/package-lock.json
generated
@@ -12,6 +12,8 @@
|
|||||||
"@sveltejs/adapter-node": "^4.0.1",
|
"@sveltejs/adapter-node": "^4.0.1",
|
||||||
"@threlte/core": "^7.1.0",
|
"@threlte/core": "^7.1.0",
|
||||||
"@threlte/extras": "^8.7.5",
|
"@threlte/extras": "^8.7.5",
|
||||||
|
"@turf/destination": "^7.1.0",
|
||||||
|
"@turf/distance": "^7.1.0",
|
||||||
"@types/chart.js": "^2.9.41",
|
"@types/chart.js": "^2.9.41",
|
||||||
"@types/leaflet.awesome-markers": "^2.0.28",
|
"@types/leaflet.awesome-markers": "^2.0.28",
|
||||||
"@types/three": "^0.161.2",
|
"@types/three": "^0.161.2",
|
||||||
@@ -1159,6 +1161,59 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@turf/destination": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@turf/destination/-/destination-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-97XuvB0iaAiMg86hrnZ529WwP44TQAA9mmI5PMlchACiA4LFrEtWjjDzvO6234coieoqhrw6dZYcJvd5O2PwrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@turf/helpers": "^7.1.0",
|
||||||
|
"@turf/invariant": "^7.1.0",
|
||||||
|
"@types/geojson": "^7946.0.10",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/turf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@turf/distance": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@turf/distance/-/distance-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-hhNHhxCHB3ddzAGCNY4BtE29OZh+DAJPvUapQz+wOjISnlwvMcwLKvslgHWSYF536QDVe/93FEU2q67+CsZTPA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@turf/helpers": "^7.1.0",
|
||||||
|
"@turf/invariant": "^7.1.0",
|
||||||
|
"@types/geojson": "^7946.0.10",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/turf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@turf/helpers": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-dTeILEUVeNbaEeoZUOhxH5auv7WWlOShbx7QSd4s0T4Z0/iz90z9yaVCtZOLbU89umKotwKaJQltBNO9CzVgaQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "^7946.0.10",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/turf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@turf/invariant": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-OCLNqkItBYIP1nE9lJGuIUatWGtQ4rhBKAyTfFu0z8npVzGEYzvguEeof8/6LkKmTTEHW53tCjoEhSSzdRh08Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@turf/helpers": "^7.1.0",
|
||||||
|
"@types/geojson": "^7946.0.10",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/turf"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/chart.js": {
|
"node_modules/@types/chart.js": {
|
||||||
"version": "2.9.41",
|
"version": "2.9.41",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.41.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.41.tgz",
|
||||||
|
|||||||
@@ -36,6 +36,8 @@
|
|||||||
"@sveltejs/adapter-node": "^4.0.1",
|
"@sveltejs/adapter-node": "^4.0.1",
|
||||||
"@threlte/core": "^7.1.0",
|
"@threlte/core": "^7.1.0",
|
||||||
"@threlte/extras": "^8.7.5",
|
"@threlte/extras": "^8.7.5",
|
||||||
|
"@turf/destination": "^7.1.0",
|
||||||
|
"@turf/distance": "^7.1.0",
|
||||||
"@types/chart.js": "^2.9.41",
|
"@types/chart.js": "^2.9.41",
|
||||||
"@types/leaflet.awesome-markers": "^2.0.28",
|
"@types/leaflet.awesome-markers": "^2.0.28",
|
||||||
"@types/three": "^0.161.2",
|
"@types/three": "^0.161.2",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import * as M from "maplibre-gl";
|
import * as M from "maplibre-gl";
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
import { createEventDispatcher, onDestroy, onMount } from "svelte";
|
import { createEventDispatcher, onDestroy, onMount } from "svelte";
|
||||||
|
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
|
||||||
|
|
||||||
export let trails: Trail[] = [];
|
export let trails: Trail[] = [];
|
||||||
export let markers: M.Marker[] = [];
|
export let markers: M.Marker[] = [];
|
||||||
@@ -22,11 +23,17 @@
|
|||||||
export let drawing: boolean = false;
|
export let drawing: boolean = false;
|
||||||
export let showElevation: boolean = true;
|
export let showElevation: boolean = true;
|
||||||
export let showInfoPopup: boolean = false;
|
export let showInfoPopup: boolean = false;
|
||||||
|
export let showGrid: boolean = false;
|
||||||
|
export let elevationProfileContainer: string | HTMLDivElement | undefined =
|
||||||
|
undefined;
|
||||||
|
export let mapOptions: Partial<M.MapOptions> | undefined = undefined;
|
||||||
|
|
||||||
export let activeTrail: number = 0;
|
export let activeTrail: number = 0;
|
||||||
export let minZoom: number = 0;
|
export let minZoom: number = 0;
|
||||||
|
|
||||||
let mapContainer: HTMLDivElement;
|
let mapContainer: HTMLDivElement;
|
||||||
let epc: ElevationProfileControl | null = null;
|
let epc: ElevationProfileControl | null = null;
|
||||||
|
let graticule: MaplibreGraticule | null = null;
|
||||||
|
|
||||||
let layers: Record<
|
let layers: Record<
|
||||||
string,
|
string,
|
||||||
@@ -78,6 +85,30 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (showGrid) {
|
||||||
|
if (!graticule) {
|
||||||
|
graticule = new MaplibreGraticule({
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 20,
|
||||||
|
showLabels: true,
|
||||||
|
labelType: "hdms",
|
||||||
|
labelSize: 10,
|
||||||
|
labelColor: "#858585",
|
||||||
|
longitudePosition: "top",
|
||||||
|
latitudePosition: "right",
|
||||||
|
paint: {
|
||||||
|
"line-opacity": 0.8,
|
||||||
|
"line-color": "rgba(0,0,0,0.2)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
map?.addControl(graticule);
|
||||||
|
} else {
|
||||||
|
if (graticule) {
|
||||||
|
map?.removeControl(graticule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getData(trails: Trail[]) {
|
function getData(trails: Trail[]) {
|
||||||
if (!trails.length) {
|
if (!trails.length) {
|
||||||
return [];
|
return [];
|
||||||
@@ -155,7 +186,7 @@
|
|||||||
? (data[activeTrail].bbox as M.LngLatBoundsLike)
|
? (data[activeTrail].bbox as M.LngLatBoundsLike)
|
||||||
: getBounds();
|
: getBounds();
|
||||||
map!.fitBounds(bounds, {
|
map!.fitBounds(bounds, {
|
||||||
animate: true,
|
animate: trails.length > 1,
|
||||||
padding: {
|
padding: {
|
||||||
top: 16,
|
top: 16,
|
||||||
left: 16,
|
left: 16,
|
||||||
@@ -410,16 +441,22 @@
|
|||||||
(s) => s.text === localStorage.getItem("layer"),
|
(s) => s.text === localStorage.getItem("layer"),
|
||||||
);
|
);
|
||||||
|
|
||||||
map = new M.Map({
|
const finalMapOptions: M.MapOptions = {
|
||||||
container: mapContainer,
|
...{
|
||||||
style:
|
container: mapContainer,
|
||||||
mapStyles[preferredMapStyleIndex].value ?? mapStyles[0].value,
|
style:
|
||||||
center: [initialState.lng, initialState.lat],
|
mapStyles[preferredMapStyleIndex].value ??
|
||||||
zoom: initialState.zoom,
|
mapStyles[0].value,
|
||||||
});
|
center: [initialState.lng, initialState.lat],
|
||||||
|
zoom: initialState.zoom,
|
||||||
|
},
|
||||||
|
...mapOptions,
|
||||||
|
};
|
||||||
|
map = new M.Map(finalMapOptions);
|
||||||
|
|
||||||
const elevationMarker = new FontawesomeMarker(
|
const elevationMarker = new FontawesomeMarker(
|
||||||
{
|
{
|
||||||
|
id: "elevation-marker",
|
||||||
icon: "fa-regular fa-circle",
|
icon: "fa-regular fa-circle",
|
||||||
fontSize: "xs",
|
fontSize: "xs",
|
||||||
width: 4,
|
width: 4,
|
||||||
@@ -449,7 +486,6 @@
|
|||||||
}),
|
}),
|
||||||
"top-left",
|
"top-left",
|
||||||
);
|
);
|
||||||
map.addControl(switcherControl);
|
|
||||||
|
|
||||||
if (showElevation) {
|
if (showElevation) {
|
||||||
epc = new ElevationProfileControl({
|
epc = new ElevationProfileControl({
|
||||||
@@ -462,6 +498,7 @@
|
|||||||
displayDistanceGrid: true,
|
displayDistanceGrid: true,
|
||||||
tooltipDisplayDPlus: false,
|
tooltipDisplayDPlus: false,
|
||||||
zoom: false,
|
zoom: false,
|
||||||
|
container: elevationProfileContainer,
|
||||||
onEnter: () => {
|
onEnter: () => {
|
||||||
elevationMarker.setOpacity("1");
|
elevationMarker.setOpacity("1");
|
||||||
},
|
},
|
||||||
@@ -474,6 +511,7 @@
|
|||||||
});
|
});
|
||||||
map.addControl(epc);
|
map.addControl(epc);
|
||||||
}
|
}
|
||||||
|
map.addControl(switcherControl);
|
||||||
|
|
||||||
map.on("styledata", () => {
|
map.on("styledata", () => {
|
||||||
trails.forEach((t, i) => {
|
trails.forEach((t, i) => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { haversineDistance } from "$lib/models/gpx/utils";
|
||||||
import type { Waypoint } from "$lib/models/waypoint";
|
import type { Waypoint } from "$lib/models/waypoint";
|
||||||
import type { LeafletEvent, Map, Marker } from "leaflet";
|
import type { LeafletEvent, Map, Marker } from "leaflet";
|
||||||
|
import * as M from "maplibre-gl";
|
||||||
|
|
||||||
export const startIcon = () => L.divIcon({
|
export const startIcon = () => L.divIcon({
|
||||||
html: '<i class="p-2 text-white bg-gray-500 rounded-full fa fa-bullseye -translate-x-1/2 -translate-y-1/3"></i>',
|
html: '<i class="p-2 text-white bg-gray-500 rounded-full fa fa-bullseye -translate-x-1/2 -translate-y-1/3"></i>',
|
||||||
@@ -64,16 +66,16 @@ export function createAnchorMarker(L: any, lat: number, lon: number, index: numb
|
|||||||
return marker
|
return marker
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculatePixelPerMeter(map: Map, meters: number) {
|
export function calculatePixelPerMeter(map: M.Map, meters: number) {
|
||||||
const y = map.getSize().y;
|
const y = map.getCanvas().getBoundingClientRect().y;
|
||||||
const x = map.getSize().x;
|
const x = map.getCanvas().getBoundingClientRect().x;
|
||||||
const maxMeters = map.containerPointToLatLng([0, y]).distanceTo(map.containerPointToLatLng([x, y]));
|
const maxMeters = map.unproject([0, y]).distanceTo(map.unproject([x, y]));
|
||||||
const pixelPerMeter = x / maxMeters;
|
const pixelPerMeter = x / maxMeters;
|
||||||
|
|
||||||
return pixelPerMeter * meters
|
return pixelPerMeter * meters
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateScaleFactor(map: Map) {
|
export function calculateScaleFactor(map: M.Map) {
|
||||||
function _pxTOmm() {
|
function _pxTOmm() {
|
||||||
let heightRef = document.createElement('div');
|
let heightRef = document.createElement('div');
|
||||||
heightRef.style.height = '1mm';
|
heightRef.style.height = '1mm';
|
||||||
@@ -89,16 +91,17 @@ export function calculateScaleFactor(map: Map) {
|
|||||||
return px / pxPermm;
|
return px / pxPermm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var centerOfMap = map.getSize().y / 2;
|
var centerOfMap = map.getCanvas().getBoundingClientRect().y / 2;
|
||||||
|
|
||||||
var realWorlMetersPer100Pixels = map.distance(
|
const p1 = map.unproject([0, centerOfMap]);
|
||||||
map.containerPointToLatLng([0, centerOfMap]),
|
const p2 = map.unproject([100, centerOfMap]);
|
||||||
map.containerPointToLatLng([100, centerOfMap])
|
var realWorldMetersPer100Pixels = haversineDistance(
|
||||||
|
p1.lat, p1.lng, p2.lat, p2.lng
|
||||||
);
|
);
|
||||||
|
|
||||||
const screenMetersPer100Pixels = _pxTOmm()(100) / 1000;
|
const screenMetersPer100Pixels = _pxTOmm()(100) / 1000;
|
||||||
|
|
||||||
const scaleFactor = realWorlMetersPer100Pixels / screenMetersPer100Pixels
|
const scaleFactor = realWorldMetersPer100Pixels / screenMetersPer100Pixels
|
||||||
|
|
||||||
return scaleFactor
|
return scaleFactor
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import { get } from "svelte/store";
|
|||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
export class FontawesomeMarker extends M.Marker {
|
export class FontawesomeMarker extends M.Marker {
|
||||||
constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string }, markerOptions?: M.MarkerOptions) {
|
constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string, id?: string }, markerOptions?: M.MarkerOptions) {
|
||||||
const element = document.createElement('div')
|
const element = document.createElement('div')
|
||||||
element.className = `cursor-pointer flex items-center justify-center w-${options.width ?? 7} aspect-square bg-${options.backgroundColor ?? "gray-500"} rounded-full text-${options.fontSize ?? "normal"}`
|
element.className = `cursor-pointer flex items-center justify-center w-${options.width ?? 7} aspect-square bg-${options.backgroundColor ?? "gray-500"} rounded-full text-${options.fontSize ?? "normal"}`
|
||||||
|
element.id = options.id ?? "";
|
||||||
super({ element: element, ...markerOptions });
|
super({ element: element, ...markerOptions });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -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);
|
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) {
|
if (this.settings.containerClass) {
|
||||||
this.profileContainer.classList.add(this.settings.containerClass);
|
this.profileContainer.classList.add(this.settings.containerClass);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -410,6 +410,7 @@ export class ElevationProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.canvas = document.createElement("canvas");
|
this.canvas = document.createElement("canvas");
|
||||||
|
this.canvas.id = "elevation-profile-chart"
|
||||||
appContainer.appendChild(this.canvas);
|
appContainer.appendChild(this.canvas);
|
||||||
|
|
||||||
Chart.register(...registerables);
|
Chart.register(...registerables);
|
||||||
@@ -515,7 +516,7 @@ export class ElevationProfile {
|
|||||||
tickColor: "#0002"
|
tickColor: "#0002"
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
stepSize: 0.1,
|
stepSize: 1,
|
||||||
align: "inner",
|
align: "inner",
|
||||||
display: this.settings.displayDistanceLabels,
|
display: this.settings.displayDistanceLabels,
|
||||||
color: this.settings.labelColor,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
import Button from "$lib/components/base/button.svelte";
|
import Button from "$lib/components/base/button.svelte";
|
||||||
import Select from "$lib/components/base/select.svelte";
|
import Select from "$lib/components/base/select.svelte";
|
||||||
import LogoText from "$lib/components/logo/logo_text.svelte";
|
import LogoText from "$lib/components/logo/logo_text.svelte";
|
||||||
import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte";
|
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
|
||||||
import type { Settings } from "$lib/models/settings";
|
import type { Settings } from "$lib/models/settings";
|
||||||
import { show_toast } from "$lib/stores/toast_store";
|
import { show_toast } from "$lib/stores/toast_store";
|
||||||
import { trail } from "$lib/stores/trail_store";
|
import { trail } from "$lib/stores/trail_store";
|
||||||
@@ -21,19 +21,16 @@
|
|||||||
} from "$lib/util/leaflet_util";
|
} from "$lib/util/leaflet_util";
|
||||||
import { createRect, createText } from "$lib/util/svg_util";
|
import { createRect, createText } from "$lib/util/svg_util";
|
||||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||||
import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule";
|
import QrCodeWithLogo from "$lib/vendor/qr-code-with-logos/index";
|
||||||
import leafletImage from "$lib/vendor/leaflet-image/leaflet-image.js";
|
|
||||||
import { Canvg } from "canvg";
|
|
||||||
import { jsPDF } from "jspdf";
|
import { jsPDF } from "jspdf";
|
||||||
import type { Map } from "leaflet";
|
|
||||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import QrCodeWithLogo from "$lib/vendor/qr-code-with-logos/index";
|
import * as M from "maplibre-gl";
|
||||||
import { onMount, tick } from "svelte";
|
import { onMount, tick } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
let map: Map;
|
let map: M.Map;
|
||||||
let graticule: AutoGraticule;
|
let showGrid: boolean = true;
|
||||||
|
|
||||||
const settings: Settings = $page.data.settings;
|
const settings: Settings = $page.data.settings;
|
||||||
|
|
||||||
@@ -94,7 +91,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function print() {
|
async function print() {
|
||||||
printLoading = true;
|
printLoading = true;
|
||||||
const doc = new jsPDF(selectedOrientation, "mm", [
|
const doc = new jsPDF(selectedOrientation, "mm", [
|
||||||
paperDimensions[selectedPaperSize].pdf[
|
paperDimensions[selectedPaperSize].pdf[
|
||||||
@@ -117,246 +114,220 @@
|
|||||||
?.getBoundingClientRect().width;
|
?.getBoundingClientRect().width;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
leafletImage(
|
var img = document.createElement("img");
|
||||||
map,
|
var dimensions = map.getCanvas().getBoundingClientRect();
|
||||||
async function (err: string, canvas: HTMLCanvasElement) {
|
img.width = dimensions.width;
|
||||||
// Map
|
img.height = dimensions.height;
|
||||||
var img = document.createElement("img");
|
let ratio = img.height / img.width;
|
||||||
var dimensions = map.getSize();
|
img.src = map.getCanvas().toDataURL();
|
||||||
img.width = dimensions.x;
|
|
||||||
img.height = dimensions.y;
|
|
||||||
const ratio = img.height / img.width;
|
|
||||||
img.src = canvas.toDataURL();
|
|
||||||
doc.addImage(img.src, "png", 8, 8, 0, (width - 16) * ratio);
|
|
||||||
currentHeight += (width - 16) * ratio + 11;
|
|
||||||
const elevationProfileHeight = currentHeight - 4;
|
|
||||||
|
|
||||||
// QR Code
|
doc.addImage(img.src, "png", 8, 8, 0, (width - 16) * ratio);
|
||||||
doc.addImage(
|
currentHeight += (width - 16) * ratio + 11;
|
||||||
document.getElementById("qrcode") as HTMLImageElement,
|
const elevationProfileHeight = currentHeight - 11;
|
||||||
"png",
|
|
||||||
8,
|
|
||||||
currentHeight,
|
|
||||||
24,
|
|
||||||
24,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Distance
|
// Waypoints
|
||||||
currentHeight += 5;
|
const canvasContainer = map.getCanvasContainer();
|
||||||
let currentWidth = 8 + 24 + 4;
|
const marker =
|
||||||
doc.setFontSize(9);
|
canvasContainer.getElementsByClassName("maplibregl-marker");
|
||||||
doc.setTextColor(107, 114, 128);
|
for (const m of marker) {
|
||||||
doc.setFont("fa-solid-900", "normal");
|
if (m.id === "elevation-marker") {
|
||||||
doc.text("\uf337", currentWidth, currentHeight);
|
continue;
|
||||||
currentWidth += doc.getTextWidth("\uf337") + 2;
|
}
|
||||||
doc.setFont("IBMPlexSans-Regular", "normal");
|
const markerStyle = window.getComputedStyle(m);
|
||||||
doc.text(
|
const matrix = new DOMMatrixReadOnly(markerStyle.transform);
|
||||||
formatDistance($trail.distance),
|
const percentX = matrix.m41 / dimensions.width;
|
||||||
currentWidth,
|
const percentY = matrix.m42 / dimensions.height;
|
||||||
currentHeight,
|
|
||||||
);
|
|
||||||
currentWidth +=
|
|
||||||
doc.getTextWidth(formatDistance($trail.distance)) + 4;
|
|
||||||
|
|
||||||
// Duration
|
const markerIcon = m.querySelector("i") as HTMLElement;
|
||||||
doc.setFont("fa-solid-900", "normal");
|
const iconContent = window
|
||||||
doc.text("\uf017", currentWidth, currentHeight);
|
.getComputedStyle(markerIcon, ":before")
|
||||||
currentWidth += doc.getTextWidth("\uf337") + 2;
|
.getPropertyValue("content");
|
||||||
doc.setFont("IBMPlexSans-Regular", "normal");
|
const iconText = String.fromCharCode(parseInt(iconContent.replace(/['"\\]/g, "").trim(), 16));
|
||||||
doc.text(
|
|
||||||
formatTimeHHMM($trail.duration),
|
|
||||||
currentWidth,
|
|
||||||
currentHeight,
|
|
||||||
);
|
|
||||||
currentWidth +=
|
|
||||||
doc.getTextWidth(formatTimeHHMM($trail.duration)) + 4;
|
|
||||||
|
|
||||||
// Elevation gain
|
doc.setFont("fa-solid-900", "normal");
|
||||||
doc.setFont("fa-solid-900", "normal");
|
doc.text(iconText, width * percentX, height * percentY);
|
||||||
doc.text("\ue098", currentWidth, currentHeight);
|
}
|
||||||
currentWidth += doc.getTextWidth("\ue098") + 2;
|
|
||||||
doc.setFont("IBMPlexSans-Regular", "normal");
|
|
||||||
doc.text(
|
|
||||||
formatElevation($trail.elevation_gain),
|
|
||||||
currentWidth,
|
|
||||||
currentHeight,
|
|
||||||
);
|
|
||||||
currentWidth +=
|
|
||||||
doc.getTextWidth(
|
|
||||||
formatDistance($trail.elevation_gain),
|
|
||||||
) + 4;
|
|
||||||
|
|
||||||
// Elevation loss
|
|
||||||
doc.setFont("fa-solid-900", "normal");
|
|
||||||
doc.text("\ue097", currentWidth, currentHeight);
|
|
||||||
currentWidth += doc.getTextWidth("\ue098") + 2;
|
|
||||||
doc.setFont("IBMPlexSans-Regular", "normal");
|
|
||||||
doc.text(
|
|
||||||
formatElevation($trail.elevation_loss),
|
|
||||||
currentWidth,
|
|
||||||
currentHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ruler
|
|
||||||
currentHeight += 6;
|
|
||||||
currentWidth = 8 + 24 + 4;
|
|
||||||
doc.setTextColor(0, 0, 0);
|
|
||||||
for (let i = 0; i < rulerSegments.length; i++) {
|
|
||||||
const segment = rulerSegments[i];
|
|
||||||
const label = rulerLabels[i].textContent ?? "";
|
|
||||||
const segmentWidth =
|
|
||||||
segment.getBoundingClientRect().width;
|
|
||||||
const segmentWidthPercent =
|
|
||||||
segmentWidth / (pageWidth ?? 0);
|
|
||||||
const segmentPDFWidth = width * segmentWidthPercent;
|
|
||||||
let textWidth = doc.getTextWidth(label);
|
|
||||||
|
|
||||||
if (i % 2 == 0) {
|
// printLoading = false;
|
||||||
doc.setFillColor(255, 255, 255);
|
// return;
|
||||||
} else {
|
// QR Code
|
||||||
doc.setFillColor(93, 93, 93);
|
doc.addImage(
|
||||||
}
|
document.getElementById("qrcode") as HTMLImageElement,
|
||||||
|
"png",
|
||||||
doc.rect(
|
8,
|
||||||
currentWidth,
|
currentHeight,
|
||||||
currentHeight,
|
24,
|
||||||
segmentPDFWidth,
|
24,
|
||||||
2,
|
|
||||||
"FD",
|
|
||||||
);
|
|
||||||
doc.text(
|
|
||||||
label,
|
|
||||||
currentWidth - textWidth / 2,
|
|
||||||
currentHeight + 6,
|
|
||||||
);
|
|
||||||
currentWidth += segmentPDFWidth;
|
|
||||||
if (i == rulerSegments.length - 1) {
|
|
||||||
const endLabel =
|
|
||||||
rulerLabels[i + 1].textContent ?? "";
|
|
||||||
textWidth = doc.getTextWidth(endLabel);
|
|
||||||
doc.text(
|
|
||||||
endLabel,
|
|
||||||
currentWidth - textWidth / 2,
|
|
||||||
currentHeight + 6,
|
|
||||||
);
|
|
||||||
textWidth = doc.getTextWidth("KM");
|
|
||||||
doc.text(
|
|
||||||
"KM",
|
|
||||||
currentWidth - textWidth,
|
|
||||||
currentHeight - 2,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scale
|
|
||||||
currentHeight += 12;
|
|
||||||
doc.setFillColor(255, 255, 255);
|
|
||||||
doc.text(
|
|
||||||
`Scale: 1 : ${Math.round(scale)}`,
|
|
||||||
8 + 24 + 4,
|
|
||||||
currentHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Elevation profile
|
|
||||||
const gridLines = document.querySelectorAll(
|
|
||||||
"#elevation .grid line",
|
|
||||||
);
|
|
||||||
gridLines.forEach((l) =>
|
|
||||||
l.setAttribute("stroke", "#d1d5db"),
|
|
||||||
);
|
|
||||||
const gridBorders = document.querySelectorAll(
|
|
||||||
"#elevation .grid path",
|
|
||||||
);
|
|
||||||
gridBorders.forEach((l) =>
|
|
||||||
l.setAttribute("stroke", "#fff"),
|
|
||||||
);
|
|
||||||
const tooltip = document.querySelector(
|
|
||||||
"#elevation .tooltip",
|
|
||||||
) as HTMLElement;
|
|
||||||
tooltip.style.display = "none";
|
|
||||||
const units = document.querySelectorAll(
|
|
||||||
"#elevation .axis text",
|
|
||||||
);
|
|
||||||
units.forEach((l) => l.setAttribute("fill", "#000"));
|
|
||||||
const elevationCanvas = document.createElement("canvas");
|
|
||||||
const ctx = elevationCanvas.getContext("2d")!;
|
|
||||||
const v = await Canvg.from(
|
|
||||||
ctx,
|
|
||||||
document.querySelector("#elevation svg")!.outerHTML,
|
|
||||||
);
|
|
||||||
v.start();
|
|
||||||
const profileRatio =
|
|
||||||
elevationCanvas.height / elevationCanvas.width;
|
|
||||||
doc.addImage(
|
|
||||||
elevationCanvas.toDataURL("image/png"),
|
|
||||||
width - 79.3 - 8,
|
|
||||||
elevationProfileHeight,
|
|
||||||
79.3,
|
|
||||||
79.3 * profileRatio,
|
|
||||||
);
|
|
||||||
const elevationPlot = (
|
|
||||||
document.querySelector(
|
|
||||||
".canvas-plot",
|
|
||||||
) as HTMLCanvasElement
|
|
||||||
).toDataURL("image/png");
|
|
||||||
doc.addImage(
|
|
||||||
elevationPlot,
|
|
||||||
width - 77.3,
|
|
||||||
elevationProfileHeight + 8,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
tooltip.style.display = "block";
|
|
||||||
|
|
||||||
// Separator
|
|
||||||
currentHeight += 5;
|
|
||||||
doc.setDrawColor(232, 234, 237);
|
|
||||||
doc.line(8, currentHeight, width - 8, currentHeight);
|
|
||||||
currentHeight += 6;
|
|
||||||
|
|
||||||
// Trail name & location
|
|
||||||
doc.setTextColor(0, 0, 0);
|
|
||||||
doc.setFont("IBMPlexSans-SemiBold", "bold");
|
|
||||||
doc.text($trail.name, 8, currentHeight);
|
|
||||||
doc.setFont("fa-solid-900", "normal");
|
|
||||||
currentHeight += 5;
|
|
||||||
doc.text("\uf3c5", 8, currentHeight);
|
|
||||||
doc.setFont("IBMPlexSans-Regular", "normal");
|
|
||||||
doc.text($trail.location || "-", 12, currentHeight);
|
|
||||||
|
|
||||||
// Logo
|
|
||||||
const logo = new Image();
|
|
||||||
logo.src = "/imgs/logo_text_dark.png";
|
|
||||||
const logoRatio = 64 / 212;
|
|
||||||
const logoWidth = 32;
|
|
||||||
const logoHeight = logoWidth * logoRatio;
|
|
||||||
doc.addImage(
|
|
||||||
logo,
|
|
||||||
width - 8 - logoWidth,
|
|
||||||
height - 4 - logoHeight,
|
|
||||||
logoWidth,
|
|
||||||
logoHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (includeDescription && $trail.description) {
|
|
||||||
doc.addPage();
|
|
||||||
doc.text($trail.description, 16, 16, {
|
|
||||||
maxWidth: width - 32,
|
|
||||||
});
|
|
||||||
doc.addImage(
|
|
||||||
logo,
|
|
||||||
width - 8 - logoWidth,
|
|
||||||
height - 4 - logoHeight,
|
|
||||||
logoWidth,
|
|
||||||
logoHeight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.save($trail.name + ".pdf");
|
|
||||||
printLoading = false;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Distance
|
||||||
|
currentHeight += 5;
|
||||||
|
let currentWidth = 8 + 24 + 4;
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setTextColor(107, 114, 128);
|
||||||
|
doc.setFont("fa-solid-900", "normal");
|
||||||
|
doc.text("\uf337", currentWidth, currentHeight);
|
||||||
|
currentWidth += doc.getTextWidth("\uf337") + 2;
|
||||||
|
doc.setFont("IBMPlexSans-Regular", "normal");
|
||||||
|
doc.text(
|
||||||
|
formatDistance($trail.distance),
|
||||||
|
currentWidth,
|
||||||
|
currentHeight,
|
||||||
|
);
|
||||||
|
currentWidth +=
|
||||||
|
doc.getTextWidth(formatDistance($trail.distance)) + 4;
|
||||||
|
|
||||||
|
// Duration
|
||||||
|
doc.setFont("fa-solid-900", "normal");
|
||||||
|
doc.text("\uf017", currentWidth, currentHeight);
|
||||||
|
currentWidth += doc.getTextWidth("\uf337") + 2;
|
||||||
|
doc.setFont("IBMPlexSans-Regular", "normal");
|
||||||
|
doc.text(
|
||||||
|
formatTimeHHMM($trail.duration),
|
||||||
|
currentWidth,
|
||||||
|
currentHeight,
|
||||||
|
);
|
||||||
|
currentWidth +=
|
||||||
|
doc.getTextWidth(formatTimeHHMM($trail.duration)) + 4;
|
||||||
|
|
||||||
|
// Elevation gain
|
||||||
|
doc.setFont("fa-solid-900", "normal");
|
||||||
|
doc.text("\ue098", currentWidth, currentHeight);
|
||||||
|
currentWidth += doc.getTextWidth("\ue098") + 2;
|
||||||
|
doc.setFont("IBMPlexSans-Regular", "normal");
|
||||||
|
doc.text(
|
||||||
|
formatElevation($trail.elevation_gain),
|
||||||
|
currentWidth,
|
||||||
|
currentHeight,
|
||||||
|
);
|
||||||
|
currentWidth +=
|
||||||
|
doc.getTextWidth(formatDistance($trail.elevation_gain)) + 4;
|
||||||
|
|
||||||
|
// Elevation loss
|
||||||
|
doc.setFont("fa-solid-900", "normal");
|
||||||
|
doc.text("\ue097", currentWidth, currentHeight);
|
||||||
|
currentWidth += doc.getTextWidth("\ue098") + 2;
|
||||||
|
doc.setFont("IBMPlexSans-Regular", "normal");
|
||||||
|
doc.text(
|
||||||
|
formatElevation($trail.elevation_loss),
|
||||||
|
currentWidth,
|
||||||
|
currentHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ruler
|
||||||
|
currentHeight += 6;
|
||||||
|
currentWidth = 8 + 24 + 4;
|
||||||
|
doc.setTextColor(0, 0, 0);
|
||||||
|
for (let i = 0; i < rulerSegments.length; i++) {
|
||||||
|
const segment = rulerSegments[i];
|
||||||
|
const label = rulerLabels[i].textContent ?? "";
|
||||||
|
const segmentWidth = segment.getBoundingClientRect().width;
|
||||||
|
const segmentWidthPercent = segmentWidth / (pageWidth ?? 0);
|
||||||
|
const segmentPDFWidth = width * segmentWidthPercent;
|
||||||
|
let textWidth = doc.getTextWidth(label);
|
||||||
|
|
||||||
|
if (i % 2 == 0) {
|
||||||
|
doc.setFillColor(255, 255, 255);
|
||||||
|
} else {
|
||||||
|
doc.setFillColor(93, 93, 93);
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.rect(currentWidth, currentHeight, segmentPDFWidth, 2, "FD");
|
||||||
|
doc.text(
|
||||||
|
label,
|
||||||
|
currentWidth - textWidth / 2,
|
||||||
|
currentHeight + 6,
|
||||||
|
);
|
||||||
|
currentWidth += segmentPDFWidth;
|
||||||
|
if (i == rulerSegments.length - 1) {
|
||||||
|
const endLabel = rulerLabels[i + 1].textContent ?? "";
|
||||||
|
textWidth = doc.getTextWidth(endLabel);
|
||||||
|
doc.text(
|
||||||
|
endLabel,
|
||||||
|
currentWidth - textWidth / 2,
|
||||||
|
currentHeight + 6,
|
||||||
|
);
|
||||||
|
textWidth = doc.getTextWidth("KM");
|
||||||
|
doc.text("KM", currentWidth - textWidth, currentHeight - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale
|
||||||
|
currentHeight += 12;
|
||||||
|
doc.setFillColor(255, 255, 255);
|
||||||
|
doc.text(
|
||||||
|
`Scale: 1 : ${Math.round(scale)}`,
|
||||||
|
8 + 24 + 4,
|
||||||
|
currentHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Elevation profile
|
||||||
|
const elevationChartCanvas = document.getElementById(
|
||||||
|
"elevation-profile-chart",
|
||||||
|
) as HTMLCanvasElement;
|
||||||
|
var dimensions = elevationChartCanvas.getBoundingClientRect();
|
||||||
|
img.width = dimensions.width;
|
||||||
|
img.height = dimensions.height;
|
||||||
|
ratio = img.height / img.width;
|
||||||
|
img.src = elevationChartCanvas.toDataURL();
|
||||||
|
|
||||||
|
doc.addImage(
|
||||||
|
elevationChartCanvas.toDataURL("image/png"),
|
||||||
|
width - 79.3 - 8,
|
||||||
|
elevationProfileHeight,
|
||||||
|
79.3,
|
||||||
|
79.3 * ratio,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
currentHeight += 5;
|
||||||
|
doc.setDrawColor(232, 234, 237);
|
||||||
|
doc.line(8, currentHeight, width - 8, currentHeight);
|
||||||
|
currentHeight += 6;
|
||||||
|
|
||||||
|
// Trail name & location
|
||||||
|
doc.setTextColor(0, 0, 0);
|
||||||
|
doc.setFont("IBMPlexSans-SemiBold", "bold");
|
||||||
|
doc.text($trail.name, 8, currentHeight);
|
||||||
|
doc.setFont("fa-solid-900", "normal");
|
||||||
|
currentHeight += 5;
|
||||||
|
doc.text("\uf3c5", 8, currentHeight);
|
||||||
|
doc.setFont("IBMPlexSans-Regular", "normal");
|
||||||
|
doc.text($trail.location || "-", 12, currentHeight);
|
||||||
|
|
||||||
|
// Logo
|
||||||
|
const logo = new Image();
|
||||||
|
logo.src = "/imgs/logo_text_dark.png";
|
||||||
|
const logoRatio = 64 / 212;
|
||||||
|
const logoWidth = 32;
|
||||||
|
const logoHeight = logoWidth * logoRatio;
|
||||||
|
doc.addImage(
|
||||||
|
logo,
|
||||||
|
width - 8 - logoWidth,
|
||||||
|
height - 4 - logoHeight,
|
||||||
|
logoWidth,
|
||||||
|
logoHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (includeDescription && $trail.description) {
|
||||||
|
doc.addPage();
|
||||||
|
doc.text($trail.description, 16, 16, {
|
||||||
|
maxWidth: width - 32,
|
||||||
|
});
|
||||||
|
doc.addImage(
|
||||||
|
logo,
|
||||||
|
width - 8 - logoWidth,
|
||||||
|
height - 4 - logoHeight,
|
||||||
|
logoWidth,
|
||||||
|
logoHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.save($trail.name + ".pdf");
|
||||||
|
printLoading = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
show_toast({
|
show_toast({
|
||||||
icon: "close",
|
icon: "close",
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -371,11 +342,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await tick();
|
await tick();
|
||||||
map.invalidateSize();
|
|
||||||
updateScale(map);
|
updateScale(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateScale(map: Map) {
|
async function updateScale(map: M.Map) {
|
||||||
const svg = document.getElementById("ruler") as HTMLElement;
|
const svg = document.getElementById("ruler") as HTMLElement;
|
||||||
if (!svg) {
|
if (!svg) {
|
||||||
return;
|
return;
|
||||||
@@ -454,9 +424,9 @@
|
|||||||
|
|
||||||
function toggleGrid() {
|
function toggleGrid() {
|
||||||
if (selectedGrid == "off") {
|
if (selectedGrid == "off") {
|
||||||
graticule?.remove();
|
showGrid = false;
|
||||||
} else {
|
} else {
|
||||||
graticule.addTo(map);
|
showGrid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -521,65 +491,52 @@
|
|||||||
]}"
|
]}"
|
||||||
>
|
>
|
||||||
<div class="basis-full">
|
<div class="basis-full">
|
||||||
<MapWithElevation
|
<MapWithElevationMaplibre
|
||||||
trail={$trail}
|
trails={[$trail]}
|
||||||
options={{
|
on:zoom={(e) => updateScale(e.detail)}
|
||||||
theme: "gray-theme",
|
|
||||||
slope: false,
|
|
||||||
speed: false,
|
|
||||||
legend: false,
|
|
||||||
time: false,
|
|
||||||
hotline: false,
|
|
||||||
graticule: true,
|
|
||||||
height: 150,
|
|
||||||
}}
|
|
||||||
on:zoomend={(e) => updateScale(e.detail)}
|
|
||||||
bind:map
|
bind:map
|
||||||
bind:graticule
|
{showGrid}
|
||||||
>
|
elevationProfileContainer="elevation-profile"
|
||||||
<img
|
mapOptions={{ preserveDrawingBuffer: true }}
|
||||||
class="w-[100px] h-[100px]"
|
></MapWithElevationMaplibre>
|
||||||
id="qrcode"
|
</div>
|
||||||
alt="QR Code"
|
<div class="flex items-center min-w-0">
|
||||||
/>
|
<img class="w-[100px] h-[100px]" id="qrcode" alt="QR Code" />
|
||||||
<div class="basis-full mx-4">
|
<div>
|
||||||
<div
|
<div
|
||||||
class="flex mt-1 gap-x-4 gap-y-2 text-sm text-gray-500 whitespace-nowrap"
|
class="flex mt-1 gap-x-4 gap-y-2 text-sm text-gray-500 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><i class="fa fa-left-right mr-2"
|
||||||
|
></i>{formatDistance($trail.distance)}</span
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
><i class="fa fa-left-right mr-2"
|
><i class="fa fa-clock mr-2"></i>{formatTimeHHMM(
|
||||||
></i>{formatDistance($trail.distance)}</span
|
$trail.duration,
|
||||||
>
|
)}</span
|
||||||
<span
|
|
||||||
><i class="fa fa-clock mr-2"
|
|
||||||
></i>{formatTimeHHMM($trail.duration)}</span
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
><i class="fa fa-arrow-trend-up mr-2"
|
|
||||||
></i>{formatElevation(
|
|
||||||
$trail.elevation_gain,
|
|
||||||
)}</span
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
><i class="fa fa-arrow-trend-down mr-2"
|
|
||||||
></i>{formatElevation(
|
|
||||||
$trail.elevation_loss,
|
|
||||||
)}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<svg
|
|
||||||
width="100%"
|
|
||||||
height="50"
|
|
||||||
id="ruler"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.1"
|
|
||||||
>
|
>
|
||||||
</svg>
|
<span
|
||||||
<span class="text-sm"
|
><i class="fa fa-arrow-trend-up mr-2"
|
||||||
>Scale:   1 : {Math.round(scale)}</span
|
></i>{formatElevation($trail.elevation_gain)}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><i class="fa fa-arrow-trend-down mr-2"
|
||||||
|
></i>{formatElevation($trail.elevation_loss)}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</MapWithElevation>
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="50"
|
||||||
|
id="ruler"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.1"
|
||||||
|
>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm"
|
||||||
|
>Scale:   1 : {Math.round(scale)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 basis-full" id="elevation-profile"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-4 -mt-4 border-t border-t-input-border z-10">
|
<div class="pt-4 -mt-4 border-t border-t-input-border z-10">
|
||||||
<div class="float-right"><LogoText height={48}></LogoText></div>
|
<div class="float-right"><LogoText height={48}></LogoText></div>
|
||||||
@@ -593,9 +550,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
|
||||||
:global(#map-container) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user