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 = ``
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 = ``; // Safe static icon
locationElement.appendChild(document.createTextNode(trail.location)); // Safely append location text
detailsContainer.appendChild(locationElement);
}
const difficultyElement = document.createElement("h5");
difficultyElement.innerHTML = ``; // 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 = ``; // 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
}