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 }