adds route editing capabilities

This commit is contained in:
Christian Beutel
2025-01-11 22:43:09 +01:00
parent a19a96f524
commit 35f12f1816
12 changed files with 469 additions and 181 deletions

View File

@@ -24,13 +24,13 @@ export function bbox(
result[3] = coord[1];
}
});
return result;
}
export function findStartAndEndPoints(geojson: GeoJsonObject): Position[] {
const startEndPoints: Position[] = [];
// Check if it's a FeatureCollection
if ((geojson as any).features) {
(geojson as any).features.forEach((feature: any) => {
@@ -50,6 +50,43 @@ export function findStartAndEndPoints(geojson: GeoJsonObject): Position[] {
return startEndPoints;
}
export function splitMultiLineStringToLineStrings(geojson: GeoJsonObject): FeatureCollection {
const features: Feature[] = [];
(geojson as any).features.forEach((feature: any) => {
if (feature.geometry.type === "MultiLineString") {
feature.geometry.coordinates.forEach((lineString: any, lineIndex: number) => {
features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: lineString,
},
properties: {
...feature.properties,
featureId: features.length,
segmentId: lineIndex,
},
});
});
} else if (feature.geometry.type === "LineString") {
features.push({
...feature,
properties: {
...feature.properties,
featureId: features.length,
segmentId: 0
},
});
}
});
return {
type: "FeatureCollection",
features: features,
};
}
function extractStartAndEndPointsFromGeometry(geometry: any, startEndPoints: Position[]) {
if (geometry.type === "LineString") {
const coords = geometry.coordinates as number[][];

View File

@@ -13,7 +13,7 @@ import GPXWaypoint from "$lib/models/gpx/waypoint";
import EasyFit from "$lib/vendor/easy-fit/easy-fit";
import type { GeoJSON, Feature, FeatureCollection, GeoJsonProperties, Position } from 'geojson';
import * as xmldom from 'xmldom';
import { bbox } from "./geojson_util";
import { bbox, splitMultiLineStringToLineStrings } from "./geojson_util";
export async function gpx2trail(gpxString: string, fallbackName?: string) {
@@ -130,7 +130,7 @@ export async function fromFile(file: File | Blob) {
});
}
return {gpxData, gpxFile};
return { gpxData, gpxFile };
}
export function fromKML(kmlData: string) {
@@ -317,9 +317,10 @@ export function isFITFile(buffer: ArrayBuffer) {
export function toGeoJson(gpxData: string) {
const parser = browser ? new DOMParser() : new xmldom.DOMParser();
const geojson = gpx(
let geojson = gpx(
parser.parseFromString(gpxData, "text/xml"),
) as GeoJSON;
geojson = splitMultiLineStringToLineStrings(geojson);
geojson.bbox = bbox(geojson)
return geojson
}

View File

@@ -1,14 +1,15 @@
import type { Trail } from "$lib/models/trail";
import type { Waypoint } from "$lib/models/waypoint";
import M from "maplibre-gl";
import { getFileURL } from "./file_util";
import { formatDistance, formatElevation, formatTimeHHMM } from "./format_util";
import { get } from "svelte/store";
import { _ } from "svelte-i18n";
import { haversineDistance } from "$lib/models/gpx/utils";
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) {
@@ -34,13 +35,32 @@ export function createMarkerFromWaypoint(waypoint: Waypoint, onDragEnd?: (marker
color: "#6b7280"
})
const popup = new M.Popup({ offset: 25, closeButton: false }).setHTML(
"<b>" +
waypoint.name +
"</b>" +
(waypoint.description && waypoint.description.length > 0
? "<br>" + waypoint.description
: ""),
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, closeButton: false }).setDOMContent(
content
);
marker
.setLngLat([waypoint.lon, waypoint.lat])
@@ -53,10 +73,11 @@ export function createMarkerFromWaypoint(waypoint: Waypoint, onDragEnd?: (marker
return marker;
}
export function createAnchorMarker(lat: number, lon: number, index: number, onDeleteClick: () => void, onDragEnd: (event: M.Marker) => void): FontawesomeMarker {
export function createAnchorMarker(lat: number, lon: number, index: number,
onDeleteClick: () => void, onDragStart: (event: Event) => void, onDragEnd: (event: Event) => void): FontawesomeMarker {
const anchorElement = document.createElement("span")
anchorElement.className = "cursor-pointer rounded-full w-6 h-6 border border-black text-center bg-background-inverse text-content-inverse"
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(
{
@@ -69,12 +90,14 @@ export function createAnchorMarker(lat: number, lon: number, index: number, onDe
const deleteButton = document.createElement("button");
deleteButton.className = "fa fa-trash text-red-500 rounded-full aspect-square h-8 text-lg";
deleteButton.addEventListener("click", onDeleteClick)
const popup = new M.Popup({})
const popup = new M.Popup({ closeButton: false })
popup.setDOMContent(deleteButton)
marker.setPopup(popup);
marker.on("dragstart", onDragStart);
marker.on("dragend", onDragEnd);
marker.getElement().addEventListener("click", (e) => {
e.preventDefault()
e.stopPropagation();
marker.togglePopup();
})
@@ -86,8 +109,8 @@ export function createPopupFromTrail(trail: Trail) {
const thumbnail = trail.photos.length
? getFileURL(trail, trail.photos[trail.thumbnail ?? 0])
: get(theme) === "light"
? emptyStateTrailLight
: emptyStateTrailDark;
? emptyStateTrailLight
: emptyStateTrailDark;
const popup = new M.Popup({ maxWidth: "320px" });
popup.setHTML(
`<a href="/map/trail/${trail.id}" data-sveltekit-preload-data="off">