265 lines
10 KiB
TypeScript
265 lines
10 KiB
TypeScript
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
|
|
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
|
|
import { haversineDistance } from "$lib/models/gpx/utils";
|
|
import type { Trail } from "$lib/models/trail";
|
|
import type { Waypoint } from "$lib/models/waypoint";
|
|
import { theme } from "$lib/stores/theme_store";
|
|
import M from "maplibre-gl";
|
|
import { _ } from "svelte-i18n";
|
|
import { get } from "svelte/store";
|
|
import { getFileURL } from "./file_util";
|
|
import { formatDistance, formatElevation, formatTimeHHMM } from "./format_util";
|
|
import { icons } from "./icon_util";
|
|
|
|
export class FontawesomeMarker extends M.Marker {
|
|
constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string, id?: string }, markerOptions?: M.MarkerOptions) {
|
|
const element = document.createElement('div')
|
|
element.className = `cursor-pointer flex items-center justify-center w-${options.width ?? 7} aspect-square ${options.backgroundColor ?? "bg-gray-500"} rounded-full text-${options.fontSize ?? "normal"}`
|
|
element.id = options.id ?? "";
|
|
super({ element: element, ...markerOptions });
|
|
|
|
const {
|
|
icon,
|
|
} = options
|
|
|
|
const iconElementString = `<i class="text-${options.fontColor ?? "white"} ${icon}"></i>`
|
|
this._element.insertAdjacentHTML('beforeend', iconElementString)
|
|
}
|
|
}
|
|
|
|
export function createMarkerFromWaypoint(waypoint: Waypoint, onDragEnd?: (marker: M.Marker, wpId?: string) => void): FontawesomeMarker {
|
|
const marker = new FontawesomeMarker({
|
|
id: waypoint.id,
|
|
icon: `fa fa-${waypoint.icon}`,
|
|
}, {
|
|
draggable: onDragEnd !== undefined,
|
|
color: "#6b7280"
|
|
|
|
})
|
|
|
|
const content = document.createElement("div");
|
|
|
|
const spanElement = document.createElement("span");
|
|
const iconElement = document.createElement("i");
|
|
const iconName = icons.includes(waypoint.icon ?? "") ? waypoint.icon : "circle";
|
|
iconElement.classList.add("fa", `fa-${iconName}`)
|
|
spanElement.appendChild(iconElement);
|
|
|
|
const nameElement = document.createElement("b");
|
|
nameElement.textContent = waypoint.name ?? "-";
|
|
if (waypoint.name?.length) {
|
|
nameElement.classList.add("ml-2")
|
|
}
|
|
spanElement.appendChild(nameElement);
|
|
content.appendChild(spanElement);
|
|
|
|
if (waypoint.description && waypoint.description.length > 0) {
|
|
const descriptionElement = document.createElement("p");
|
|
descriptionElement.textContent = waypoint.description;
|
|
content.appendChild(descriptionElement);
|
|
}
|
|
|
|
|
|
const popup = new M.Popup({ offset: 25 }).setDOMContent(
|
|
content
|
|
);
|
|
marker
|
|
.setLngLat([waypoint.lon, waypoint.lat])
|
|
.setPopup(popup)
|
|
|
|
if (onDragEnd) {
|
|
marker.on("dragend", () => onDragEnd(marker, waypoint.id,));
|
|
}
|
|
|
|
return marker;
|
|
}
|
|
|
|
export function createAnchorMarker(lat: number, lon: number, index: number,
|
|
onDeleteClick: () => void, onLoopClick: () => void,
|
|
onDragStart: (event: Event) => void, onDragEnd: (event: Event) => void): FontawesomeMarker {
|
|
|
|
const anchorElement = document.createElement("span")
|
|
anchorElement.className = "route-anchor cursor-pointer rounded-full w-6 h-6 border border-black text-center bg-background-inverse text-content-inverse"
|
|
anchorElement.textContent = "" + index
|
|
const marker = new M.Marker(
|
|
{
|
|
draggable: true,
|
|
element: anchorElement
|
|
}
|
|
);
|
|
marker.setLngLat([lon, lat]);
|
|
const popup = new M.Popup()
|
|
|
|
const popupContent = document.createElement("div");
|
|
const anchorH = document.createElement("h5")
|
|
anchorH.classList.add("text-base", "font-medium");
|
|
anchorH.textContent = get(_)("route-point") + " #" + index;
|
|
|
|
const deleteButton = document.createElement("button");
|
|
deleteButton.className = "btn-secondary w-full mt-2 text-sm";
|
|
const deleteButtonIcon = document.createElement("i")
|
|
deleteButtonIcon.classList.add("fa", "fa-trash", "mr-2")
|
|
deleteButton.appendChild(deleteButtonIcon)
|
|
const deleteButtonText = document.createElement("span")
|
|
deleteButtonText.textContent = get(_)("delete")
|
|
deleteButton.appendChild(deleteButtonIcon)
|
|
deleteButton.appendChild(deleteButtonText)
|
|
deleteButton.addEventListener("click", onDeleteClick)
|
|
|
|
const loopButton = document.createElement("button");
|
|
loopButton.className = "btn-secondary w-full mt-2 text-sm block";
|
|
const loopButtonIcon = document.createElement("i")
|
|
loopButtonIcon.classList.add("fa", "fa-person-walking-arrow-loop-left", "mr-2")
|
|
loopButton.appendChild(loopButtonIcon)
|
|
const loopButtonText = document.createElement("span")
|
|
loopButtonText.textContent = get(_)("loop")
|
|
loopButton.appendChild(loopButtonIcon)
|
|
loopButton.appendChild(loopButtonText)
|
|
loopButton.addEventListener("click", onLoopClick)
|
|
|
|
popupContent.appendChild(anchorH)
|
|
popupContent.appendChild(deleteButton)
|
|
popupContent.appendChild(loopButton)
|
|
popup.setDOMContent(popupContent)
|
|
marker.setPopup(popup);
|
|
|
|
marker.on("dragstart", onDragStart);
|
|
marker.on("dragend", onDragEnd);
|
|
marker.getElement().addEventListener("click", (e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation();
|
|
marker.togglePopup();
|
|
})
|
|
|
|
return marker
|
|
}
|
|
|
|
export function createPopupFromTrail(trail: Trail) {
|
|
const thumbnail = trail.photos.length
|
|
? getFileURL(trail, trail.photos[trail.thumbnail ?? 0])
|
|
: get(theme) === "light"
|
|
? emptyStateTrailLight
|
|
: emptyStateTrailDark;
|
|
const popup = new M.Popup({ maxWidth: "320px" });
|
|
// Create a container element for the popup content
|
|
const linkElement = document.createElement("a");
|
|
linkElement.href = `/map/trail/${trail.id}`; // Set href safely
|
|
linkElement.setAttribute("data-sveltekit-preload-data", "off");
|
|
|
|
// Create a list item element
|
|
const listItem = document.createElement("li");
|
|
listItem.className = "flex items-center gap-4 cursor-pointer text-content max-w-80";
|
|
|
|
// Create the image container
|
|
const imageContainer = document.createElement("div");
|
|
imageContainer.className = "shrink-0";
|
|
|
|
// Create the image element
|
|
const img = document.createElement("img");
|
|
img.className = "h-14 w-14 object-cover rounded-xl";
|
|
img.src = thumbnail; // Set image source safely
|
|
img.alt = ""; // Always include a safe alt attribute
|
|
imageContainer.appendChild(img);
|
|
|
|
// Create the text container
|
|
const textContainer = document.createElement("div");
|
|
|
|
// Add trail name
|
|
const trailName = document.createElement("h4");
|
|
trailName.className = "font-semibold text-lg";
|
|
trailName.textContent = trail.name; // Set trail name safely
|
|
textContainer.appendChild(trailName);
|
|
|
|
// Add location and difficulty, if available
|
|
if (trail.location || trail.difficulty) {
|
|
const detailsContainer = document.createElement("div");
|
|
detailsContainer.className = "flex gap-x-4";
|
|
|
|
if (trail.location) {
|
|
const locationElement = document.createElement("h5");
|
|
locationElement.innerHTML = `<i class="fa fa-location-dot mr-2"></i>`; // Safe static icon
|
|
locationElement.appendChild(document.createTextNode(trail.location)); // Safely append location text
|
|
detailsContainer.appendChild(locationElement);
|
|
}
|
|
|
|
const difficultyElement = document.createElement("h5");
|
|
difficultyElement.innerHTML = `<i class="fa fa-gauge mr-2"></i>`; // Safe static icon
|
|
difficultyElement.appendChild(document.createTextNode(get(_)(trail.difficulty as string))); // Safely append difficulty
|
|
detailsContainer.appendChild(difficultyElement);
|
|
|
|
textContainer.appendChild(detailsContainer);
|
|
}
|
|
|
|
// Create the grid container for additional stats
|
|
const statsContainer = document.createElement("div");
|
|
statsContainer.className =
|
|
"grid grid-cols-2 mt-2 gap-x-4 gap-y-2 text-sm text-gray-500 flex-wrap";
|
|
|
|
const stats = [
|
|
{ icon: "fa-left-right", value: formatDistance(trail.distance) },
|
|
{ icon: "fa-clock", value: formatTimeHHMM(trail.duration) },
|
|
{ icon: "fa-arrow-trend-up", value: formatElevation(trail.elevation_gain) },
|
|
{ icon: "fa-arrow-trend-down", value: formatElevation(trail.elevation_loss) },
|
|
];
|
|
|
|
// Loop through stats and add them
|
|
stats.forEach(({ icon, value }) => {
|
|
const statElement = document.createElement("span");
|
|
statElement.className = "shrink-0";
|
|
statElement.innerHTML = `<i class="fa ${icon} mr-2"></i>`; // Safe static icon
|
|
statElement.appendChild(document.createTextNode(value)); // Safely append stat value
|
|
statsContainer.appendChild(statElement);
|
|
});
|
|
|
|
textContainer.appendChild(statsContainer);
|
|
|
|
// Assemble the popup
|
|
listItem.appendChild(imageContainer);
|
|
listItem.appendChild(textContainer);
|
|
linkElement.appendChild(listItem);
|
|
|
|
// Safely set the content using setDOMContent
|
|
popup.setDOMContent(linkElement);
|
|
|
|
return popup;
|
|
}
|
|
|
|
export function calculatePixelPerMeter(map: M.Map, meters: number) {
|
|
const y = map.getCanvas().getBoundingClientRect().y;
|
|
const x = map.getCanvas().getBoundingClientRect().x;
|
|
const maxMeters = map.unproject([0, y]).distanceTo(map.unproject([x, y]));
|
|
const pixelPerMeter = x / maxMeters;
|
|
|
|
return pixelPerMeter * meters
|
|
}
|
|
|
|
export function calculateScaleFactor(map: M.Map) {
|
|
function _pxTOmm() {
|
|
let heightRef = document.createElement('div');
|
|
heightRef.style.height = '1mm';
|
|
heightRef.style.position = "absolute";
|
|
heightRef.id = 'heightRef';
|
|
document.body.appendChild(heightRef);
|
|
|
|
const pxPermm = heightRef.getBoundingClientRect().height;
|
|
|
|
document.body.removeChild(heightRef);
|
|
|
|
return function pxTOmm(px: number) {
|
|
return px / pxPermm;
|
|
}
|
|
}
|
|
var centerOfMap = map.getCanvas().getBoundingClientRect().y / 2;
|
|
|
|
const p1 = map.unproject([0, centerOfMap]);
|
|
const p2 = map.unproject([100, centerOfMap]);
|
|
var realWorldMetersPer100Pixels = haversineDistance(
|
|
p1.lat, p1.lng, p2.lat, p2.lng
|
|
);
|
|
|
|
const screenMetersPer100Pixels = _pxTOmm()(100) / 1000;
|
|
|
|
const scaleFactor = realWorldMetersPer100Pixels / screenMetersPer100Pixels
|
|
|
|
return scaleFactor
|
|
} |