@@ -68,7 +68,7 @@
|
||||
<li
|
||||
class="menu-item flex items-center px-4 py-3 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
||||
role="presentation"
|
||||
on:mouseup|stopPropagation={() => handleItemClick(item)}
|
||||
on:mousedown|stopPropagation={() => handleItemClick(item)}
|
||||
>
|
||||
{#if item.icon}
|
||||
<i class="fa fa-{item.icon} mr-3"></i>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export let src: string;
|
||||
export let isThumbnail: boolean = false;
|
||||
export let showThumbnailControls: boolean = true;
|
||||
export let showExifControls: boolean = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -12,6 +13,10 @@
|
||||
dispatch("thumbnail", src);
|
||||
}
|
||||
|
||||
function handleExifClick() {
|
||||
dispatch("exif", src);
|
||||
}
|
||||
|
||||
function handleDeleteClick() {
|
||||
dispatch("delete", src);
|
||||
}
|
||||
@@ -38,7 +43,15 @@
|
||||
><i class="fa fa-file-image text-primary"></i></button
|
||||
>
|
||||
{/if}
|
||||
|
||||
{#if showExifControls}
|
||||
<button
|
||||
type="button"
|
||||
class="tooltip"
|
||||
data-title={$_("get-position-from-exif")}
|
||||
on:click={handleExifClick}
|
||||
><i class="fa fa-magnifying-glass-location text-primary"></i></button
|
||||
>
|
||||
{/if}
|
||||
<button
|
||||
class="tooltip"
|
||||
data-title={$_("delete")}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
||||
|
||||
export let trail: Trail | null;
|
||||
export let markers: Marker[] = [];
|
||||
@@ -22,15 +24,22 @@
|
||||
let controlElevation: any;
|
||||
|
||||
$: gpxData = trail?.expand.gpx_data;
|
||||
$: if (gpxData && controlElevation) {
|
||||
$: if (gpxData && controlElevation) {
|
||||
controlElevation.clear();
|
||||
controlElevation.load(gpxData);
|
||||
}
|
||||
|
||||
$: if(options) {
|
||||
controlElevation?.updateOptions(options)
|
||||
$: if (options) {
|
||||
controlElevation?.updateOptions(options);
|
||||
}
|
||||
|
||||
const hotlineSwitcherItems: DropdownItem[] = [
|
||||
{ text: $_("altitude"), value: "altitude" },
|
||||
{ text: $_("slope"), value: "slope" },
|
||||
{ text: $_("speed"), value: "speed" },
|
||||
{ text: $_("off"), value: false },
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
L = (await import("leaflet")).default;
|
||||
await import("leaflet-gpx");
|
||||
@@ -55,9 +64,39 @@
|
||||
dispatch("click", e);
|
||||
});
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
}).addTo(map);
|
||||
const baseLayer = L.tileLayer(
|
||||
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
{
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
},
|
||||
);
|
||||
|
||||
const topoLayer = L.tileLayer(
|
||||
"https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png ",
|
||||
{
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
},
|
||||
);
|
||||
|
||||
const baseMaps = {
|
||||
OpenStreetMaps: baseLayer,
|
||||
OpenTopoMaps: topoLayer,
|
||||
};
|
||||
|
||||
L.control.layers(baseMaps).addTo(map);
|
||||
|
||||
switch (localStorage.getItem("layer")) {
|
||||
case "OpenTopoMaps":
|
||||
topoLayer.addTo(map);
|
||||
break;
|
||||
default:
|
||||
baseLayer.addTo(map);
|
||||
break;
|
||||
}
|
||||
|
||||
map!.on("baselayerchange", function (e) {
|
||||
localStorage.setItem("layer", e.name);
|
||||
});
|
||||
|
||||
const default_elevation_options = {
|
||||
height: 200,
|
||||
@@ -84,11 +123,16 @@
|
||||
// Toggle "leaflet-almostover" integration
|
||||
almostOver: false,
|
||||
// Toggle "leaflet-distance-markers" integration
|
||||
distanceMarkers: { lazy: false, distance: false, direction: true, offset: 500 },
|
||||
distanceMarkers: {
|
||||
lazy: false,
|
||||
distance: false,
|
||||
direction: true,
|
||||
offset: 500,
|
||||
},
|
||||
// Toggle "leaflet-edgescale" integration
|
||||
edgeScale: false,
|
||||
// Toggle "leaflet-hotline" integration
|
||||
hotline: true,
|
||||
hotline: "altitude",
|
||||
// Display track datetimes: true || false
|
||||
timestamps: false,
|
||||
waypoints: false,
|
||||
@@ -103,7 +147,7 @@
|
||||
prefix: "fa",
|
||||
markerColor: "cadetblue",
|
||||
iconColor: "white",
|
||||
className: "awesome-marker pointer-events-none"
|
||||
className: "awesome-marker pointer-events-none",
|
||||
}),
|
||||
},
|
||||
trkEnd: {
|
||||
@@ -112,11 +156,11 @@
|
||||
prefix: "fa",
|
||||
markerColor: "cadetblue",
|
||||
iconColor: "white",
|
||||
className: "awesome-marker pointer-events-none"
|
||||
className: "awesome-marker pointer-events-none",
|
||||
}),
|
||||
},
|
||||
graticule: false,
|
||||
drawing: false
|
||||
drawing: false,
|
||||
};
|
||||
|
||||
const elevation_options = Object.assign(
|
||||
@@ -137,16 +181,37 @@
|
||||
graticule.addTo(map!);
|
||||
}
|
||||
});
|
||||
|
||||
function switchHotline(metric: "altitude" | "slope" | "speed" | false) {
|
||||
controlElevation.updateOptions({ hotline: metric, autofitBounds: false });
|
||||
controlElevation.clear();
|
||||
controlElevation.load(gpxData);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="map-container" class="flex flex-col h-full">
|
||||
<div
|
||||
id="map"
|
||||
class="rounded-xl z-0 basis-full min-h-72"
|
||||
class="rounded-xl z-0 basis-full min-h-96 md:min-h-0"
|
||||
style={crosshair
|
||||
? "position: relative; outline-style: none;cursor: crosshair !important"
|
||||
: "position: relative; outline-style: none;"}
|
||||
></div>
|
||||
>
|
||||
<div class="absolute top-20 right-3 text-sm" style="z-index: 500">
|
||||
<Dropdown
|
||||
items={hotlineSwitcherItems}
|
||||
on:change={(e) => switchHotline(e.detail.value)}
|
||||
let:toggleMenu={openDropdown}
|
||||
>
|
||||
<button
|
||||
class="rounded-sm bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-11 w-11"
|
||||
on:click={openDropdown}
|
||||
>
|
||||
<i class="fa fa-route text-lg"></i>
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<slot />
|
||||
<div class="basis-[300px] flex-grow flex-shrink-0" id="elevation"></div>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
export let parent: { [key: string]: any };
|
||||
export let thumbnail: number = 0;
|
||||
export let showThumbnailControls: boolean = true;
|
||||
export let showExifControls: boolean = false;
|
||||
|
||||
let photoPreviews: string[] = [];
|
||||
|
||||
@@ -15,7 +16,7 @@
|
||||
(photoFiles ?? []).map(async (f) => {
|
||||
return await readAsDataURLAsync(f);
|
||||
}),
|
||||
).then((v) => {
|
||||
).then((v) => {
|
||||
photoPreviews = v;
|
||||
});
|
||||
|
||||
@@ -59,7 +60,7 @@
|
||||
if (!photoFiles) {
|
||||
photoFiles = [];
|
||||
}
|
||||
photoFiles = [...photoFiles, file]
|
||||
photoFiles = [...photoFiles, file];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +118,9 @@
|
||||
on:delete={() => handlePhotoDelete(i)}
|
||||
isThumbnail={thumbnail === i}
|
||||
on:thumbnail={() => makePhotoThumbnail(i)}
|
||||
on:exif
|
||||
{showThumbnailControls}
|
||||
{showExifControls}
|
||||
></PhotoCard>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -35,9 +35,21 @@
|
||||
|
||||
onMount(() => {
|
||||
const storedDisplayOption = localStorage.getItem("displayOption");
|
||||
const storedSort= localStorage.getItem("sort");
|
||||
const storedSortOrder= localStorage.getItem("sort_order");
|
||||
|
||||
if (storedDisplayOption) {
|
||||
selectedDisplayOption = storedDisplayOption;
|
||||
}
|
||||
if(storedSort) {
|
||||
filter.sort = storedSort as typeof filter.sort
|
||||
}
|
||||
if(storedSortOrder) {
|
||||
filter.sortOrder = storedSortOrder as typeof filter.sortOrder
|
||||
}
|
||||
if(storedSort || storedSortOrder) {
|
||||
dispatch("update", filter);
|
||||
}
|
||||
});
|
||||
|
||||
function setDisplayOption() {
|
||||
@@ -45,6 +57,7 @@
|
||||
}
|
||||
|
||||
function setSort() {
|
||||
localStorage.setItem("sort", filter.sort)
|
||||
dispatch("update", filter);
|
||||
}
|
||||
|
||||
@@ -54,6 +67,7 @@
|
||||
} else {
|
||||
filter.sortOrder = "+";
|
||||
}
|
||||
localStorage.setItem("sort_order", filter.sortOrder)
|
||||
dispatch("update", filter);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { waypoint } from "$lib/stores/waypoint_store";
|
||||
import { icons } from "$lib/util/icon_util";
|
||||
import EXIF from "$lib/vendor/exif-js/exif";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib/index";
|
||||
import { util } from "$lib/vendor/svelte-form-lib/util";
|
||||
import { _ } from "svelte-i18n";
|
||||
@@ -12,6 +13,9 @@
|
||||
import TextField from "../base/text_field.svelte";
|
||||
import Textarea from "../base/textarea.svelte";
|
||||
import PhotoPicker from "../trail/photo_picker.svelte";
|
||||
import { convertDMSToDD } from "$lib/util/leaflet_util";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
|
||||
export let openModal: (() => void) | undefined = undefined;
|
||||
export let closeModal: (() => void) | undefined = undefined;
|
||||
|
||||
@@ -20,9 +24,9 @@
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<Waypoint>({
|
||||
initialValues: $waypoint,
|
||||
validationSchema: waypointSchema,
|
||||
onSubmit: async (submittedWaypoint) => {
|
||||
dispatch("save", submittedWaypoint);
|
||||
|
||||
onSubmit: async (submittedWaypoint) => {
|
||||
dispatch("save", submittedWaypoint);
|
||||
|
||||
closeModal!();
|
||||
},
|
||||
});
|
||||
@@ -35,6 +39,26 @@
|
||||
.filter((i) => i.includes($form.icon ?? ""))
|
||||
.map((i) => ({ text: i, value: i, icon: i }))
|
||||
: [];
|
||||
|
||||
function getCoordinatesFromPhoto(src: string) {
|
||||
EXIF.getData({ src: src }, function (p) {
|
||||
const lat = EXIF.getTag(p, "GPSLatitude");
|
||||
const latDir = EXIF.getTag(p, "GPSLatitudeRef");
|
||||
const lon = EXIF.getTag(p, "GPSLongitude");
|
||||
const lonDir = EXIF.getTag(p, "GPSLongitudeRef");
|
||||
|
||||
if (lat && lon) {
|
||||
$form.lat = convertDMSToDD(lat, latDir);
|
||||
$form.lon = convertDMSToDD(lon, lonDir);
|
||||
} else {
|
||||
show_toast({
|
||||
text: "No GPS data in image",
|
||||
icon: "close",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
@@ -102,9 +126,11 @@
|
||||
<PhotoPicker
|
||||
id="waypoint"
|
||||
parent={$form}
|
||||
on:exif={(e) => getCoordinatesFromPhoto(e.detail)}
|
||||
bind:photos={$form.photos}
|
||||
bind:photoFiles={$form._photos}
|
||||
showThumbnailControls={false}
|
||||
showExifControls={true}
|
||||
></PhotoPicker>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user