adds map view for lists
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
FROM getmeili/meilisearch:v1.6.2
|
FROM getmeili/meilisearch:v1.7
|
||||||
|
|
||||||
COPY ./migrations/migration.dump /meili_data/dumps/migration.dump
|
COPY ./migrations/migration.dump /meili_data/dumps/migration.dump
|
||||||
COPY ./entrypoint.sh /entrypoint.sh
|
COPY ./entrypoint.sh /entrypoint.sh
|
||||||
|
|||||||
Binary file not shown.
@@ -43,7 +43,7 @@
|
|||||||
reverseCoords: false,
|
reverseCoords: false,
|
||||||
acceleration: false,
|
acceleration: false,
|
||||||
slope: true,
|
slope: true,
|
||||||
speed: false,
|
speed: true,
|
||||||
altitude: true,
|
altitude: true,
|
||||||
time: true,
|
time: true,
|
||||||
distance: true,
|
distance: true,
|
||||||
|
|||||||
@@ -54,10 +54,12 @@
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDifficultyFilter(difficulty: "easy" | "moderate" | "difficult") {
|
function setDifficultyFilter(
|
||||||
|
difficulty: "easy" | "moderate" | "difficult",
|
||||||
|
) {
|
||||||
const difficultyIndex = filter.difficulty.findIndex(
|
const difficultyIndex = filter.difficulty.findIndex(
|
||||||
(d) => d == difficulty,
|
(d) => d == difficulty,
|
||||||
);
|
);
|
||||||
if (difficultyIndex !== -1) {
|
if (difficultyIndex !== -1) {
|
||||||
filter.difficulty.splice(difficultyIndex, 1);
|
filter.difficulty.splice(difficultyIndex, 1);
|
||||||
} else {
|
} else {
|
||||||
@@ -102,8 +104,9 @@
|
|||||||
|
|
||||||
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
||||||
text: h.name,
|
text: h.name,
|
||||||
description:
|
description: `${h.division} | ${
|
||||||
country_codes[h["country code"] as keyof typeof country_codes],
|
country_codes[h["country code"] as keyof typeof country_codes]
|
||||||
|
}`,
|
||||||
value: h,
|
value: h,
|
||||||
icon: "city",
|
icon: "city",
|
||||||
}));
|
}));
|
||||||
@@ -208,7 +211,12 @@
|
|||||||
></DoubleSlider>
|
></DoubleSlider>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>{formatDistance(filter.distanceMin)}</span>
|
<span>{formatDistance(filter.distanceMin)}</span>
|
||||||
<span>{formatDistance(filter.distanceMax)}{filter.distanceMax == filter.distanceLimit ? '+' : ''}</span>
|
<span
|
||||||
|
>{formatDistance(filter.distanceMax)}{filter.distanceMax ==
|
||||||
|
filter.distanceLimit
|
||||||
|
? "+"
|
||||||
|
: ""}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-4 border-separator" />
|
<hr class="my-4 border-separator" />
|
||||||
<p class="text-sm font-medium pb-4">{$_("elevation-gain")}</p>
|
<p class="text-sm font-medium pb-4">{$_("elevation-gain")}</p>
|
||||||
@@ -221,7 +229,13 @@
|
|||||||
></DoubleSlider>
|
></DoubleSlider>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>{formatElevation(filter.elevationGainMin)}</span>
|
<span>{formatElevation(filter.elevationGainMin)}</span>
|
||||||
<span>{formatElevation(filter.elevationGainMax)}{filter.elevationGainMax == filter.elevationGainLimit ? '+' : ''}</span>
|
<span
|
||||||
|
>{formatElevation(
|
||||||
|
filter.elevationGainMax,
|
||||||
|
)}{filter.elevationGainMax == filter.elevationGainLimit
|
||||||
|
? "+"
|
||||||
|
: ""}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-4 border-separator" />
|
<hr class="my-4 border-separator" />
|
||||||
<p class="text-sm font-medium pb-4">{$_("completed")}</p>
|
<p class="text-sm font-medium pb-4">{$_("completed")}</p>
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ export async function trails_delete(trail: Trail) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchGPX(trail: Trail, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
export async function fetchGPX(trail: Trail, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||||
if (!trail.gpx) {
|
if (!trail.gpx) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,4 +331,9 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: #000;
|
color: #000;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-content {
|
||||||
|
width: max-content !important;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
const cityItems = response.results[1].hits.map(
|
const cityItems = response.results[1].hits.map(
|
||||||
(c: Record<string, any>) => ({
|
(c: Record<string, any>) => ({
|
||||||
text: c.name,
|
text: c.name,
|
||||||
description: `City | ${
|
description: `City | ${c.division} | ${
|
||||||
country_codes[
|
country_codes[
|
||||||
c["country code"] as keyof typeof country_codes
|
c["country code"] as keyof typeof country_codes
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
const sort = event.url.searchParams.get('sort') ?? ""
|
const sort = event.url.searchParams.get('sort') ?? ""
|
||||||
try {
|
try {
|
||||||
const r: List[] = await pb.collection('lists').getFullList<List>({
|
const r: List[] = await pb.collection('lists').getFullList<List>({
|
||||||
expand: "trails",
|
expand: "trails,trails.waypoints,trails.category",
|
||||||
sort: sort,
|
sort: sort,
|
||||||
})
|
})
|
||||||
return json(r)
|
return json(r)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import ListModal from "$lib/components/list/list_modal.svelte";
|
import ListModal from "$lib/components/list/list_modal.svelte";
|
||||||
import TrailList from "$lib/components/trail/trail_list.svelte";
|
import TrailList from "$lib/components/trail/trail_list.svelte";
|
||||||
import { List } from "$lib/models/list";
|
import { List } from "$lib/models/list";
|
||||||
import type { TrailFilter } from "$lib/models/trail";
|
import type { Trail, TrailFilter } from "$lib/models/trail";
|
||||||
import {
|
import {
|
||||||
list,
|
list,
|
||||||
lists,
|
lists,
|
||||||
@@ -15,15 +15,129 @@
|
|||||||
lists_index,
|
lists_index,
|
||||||
lists_update,
|
lists_update,
|
||||||
} from "$lib/stores/list_store";
|
} from "$lib/stores/list_store";
|
||||||
|
import { fetchGPX } from "$lib/stores/trail_store";
|
||||||
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
|
import {
|
||||||
|
formatDistance,
|
||||||
|
formatElevation,
|
||||||
|
formatTimeHHMM,
|
||||||
|
} from "$lib/util/format_util";
|
||||||
|
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||||
|
import type {
|
||||||
|
GPX,
|
||||||
|
Icon,
|
||||||
|
LatLngBoundsExpression,
|
||||||
|
LeafletEvent,
|
||||||
|
Map,
|
||||||
|
Marker,
|
||||||
|
} from "leaflet";
|
||||||
|
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
|
||||||
|
import { onMount, tick } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
let openListModal: () => void;
|
let openListModal: () => void;
|
||||||
let openConfirmModal: () => void;
|
let openConfirmModal: () => void;
|
||||||
|
|
||||||
let filter: TrailFilter = $page.data.filter;
|
let filter: TrailFilter = $page.data.filter;
|
||||||
|
|
||||||
let listToBeDeleted: List | null = null;
|
let listToBeDeleted: List | null = null;
|
||||||
|
|
||||||
|
let L: any;
|
||||||
|
let map: Map;
|
||||||
|
let showMap: boolean = false;
|
||||||
|
let gpxLayers: GPX[] = [];
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
L = (await import("leaflet")).default;
|
||||||
|
await import("leaflet-gpx");
|
||||||
|
await import("leaflet.awesome-markers");
|
||||||
|
|
||||||
|
map = L.map("map").setView([0, 0], 4);
|
||||||
|
map.attributionControl.setPrefix(false);
|
||||||
|
|
||||||
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
|
attribution: "© OpenStreetMap contributors",
|
||||||
|
}).addTo(map);
|
||||||
|
});
|
||||||
|
|
||||||
|
function addGPXLayer(trail: Trail) {
|
||||||
|
return new Promise<GPX>(function (resolve, reject) {
|
||||||
|
if (!trail.expand.gpx_data) {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnail = trail.photos.length
|
||||||
|
? getFileURL(trail, trail.photos[trail.thumbnail])
|
||||||
|
: "/imgs/default_thumbnail.webp";
|
||||||
|
const gpxLayer = new L.GPX(trail.expand.gpx_data!, {
|
||||||
|
async: true,
|
||||||
|
polyline_options: {
|
||||||
|
className: "lightblue-theme elevation-polyline",
|
||||||
|
weight: 5,
|
||||||
|
},
|
||||||
|
gpx_options: {
|
||||||
|
parseElements: ["track"],
|
||||||
|
},
|
||||||
|
marker_options: {
|
||||||
|
startIcon: L.AwesomeMarkers.icon({
|
||||||
|
icon: "circle-half-stroke",
|
||||||
|
prefix: "fa",
|
||||||
|
markerColor: "cadetblue",
|
||||||
|
iconColor: "white",
|
||||||
|
}) as Icon,
|
||||||
|
startIconUrl: "",
|
||||||
|
endIconUrl: "",
|
||||||
|
shadowUrl: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.on("addpoint", function (e: any) {
|
||||||
|
console.log(e);
|
||||||
|
|
||||||
|
if (e.point_type === "start") {
|
||||||
|
const marker: Marker = e.point as Marker;
|
||||||
|
marker.bindPopup(
|
||||||
|
`<a href="map/trail/${trail.id}">
|
||||||
|
<li class="flex items-center gap-4 cursor-pointer text-black">
|
||||||
|
<div class="shrink-0"><img class="h-14 w-14 object-cover rounded-xl" src="${thumbnail}" alt="">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold text-lg">${trail.name}</h4>
|
||||||
|
<div class="flex gap-x-4">
|
||||||
|
${trail.location ? `<h5><i class="fa fa-location-dot mr-2"></i>${trail.location}</h5>` : ""}
|
||||||
|
<h5><i class="fa fa-person-hiking mr-2"></i>${$_(trail.difficulty as string)}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="flex mt-2 gap-4 text-sm text-gray-500"><span class="shrink-0"><i
|
||||||
|
class="fa fa-left-right mr-2"></i>${formatDistance(
|
||||||
|
trail.distance,
|
||||||
|
)}</span> <span class="shrink-0"><i class="fa fa-up-down mr-2"></i>${formatElevation(
|
||||||
|
trail.elevation_gain,
|
||||||
|
)}</span> <span class="shrink-0"><i class="fa fa-clock mr-2"></i>${formatTimeHHMM(
|
||||||
|
trail.duration,
|
||||||
|
)}</span></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</a>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("loaded", function (e: LeafletEvent) {
|
||||||
|
resolve(gpxLayer);
|
||||||
|
})
|
||||||
|
.on("error", reject)
|
||||||
|
.addTo(map);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleMap() {
|
||||||
|
showMap = !showMap;
|
||||||
|
if (showMap) {
|
||||||
|
setCurrentList($list);
|
||||||
|
await tick();
|
||||||
|
map.invalidateSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function beforeListModalOpen() {
|
function beforeListModalOpen() {
|
||||||
list.set(new List("", []));
|
list.set(new List("", []));
|
||||||
openListModal();
|
openListModal();
|
||||||
@@ -62,6 +176,37 @@
|
|||||||
await lists_index();
|
await lists_index();
|
||||||
listToBeDeleted = null;
|
listToBeDeleted = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setCurrentList(item: List) {
|
||||||
|
for (const layer of gpxLayers) {
|
||||||
|
map.removeLayer(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.set(item);
|
||||||
|
|
||||||
|
if (item.expand && item.expand.trails.length > 0) {
|
||||||
|
let minLat = item.expand.trails[0].lat!;
|
||||||
|
let maxLat = item.expand.trails[0].lat!;
|
||||||
|
let minLon = item.expand.trails[0].lon!;
|
||||||
|
let maxLon = item.expand.trails[0].lon!;
|
||||||
|
|
||||||
|
for (const trail of item.expand.trails) {
|
||||||
|
minLat = Math.min(minLat, trail.lat!);
|
||||||
|
maxLat = Math.max(maxLat, trail.lat!);
|
||||||
|
minLon = Math.min(minLon, trail.lon!);
|
||||||
|
maxLon = Math.max(maxLon, trail.lon!);
|
||||||
|
|
||||||
|
const gpxData: string = await fetchGPX(trail);
|
||||||
|
trail.expand.gpx_data = gpxData;
|
||||||
|
gpxLayers.push(await addGPXLayer(trail));
|
||||||
|
}
|
||||||
|
const boundingBox: LatLngBoundsExpression = [
|
||||||
|
[maxLat, minLon],
|
||||||
|
[minLat, maxLon],
|
||||||
|
];
|
||||||
|
map.fitBounds(boundingBox);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -72,20 +217,31 @@
|
|||||||
style="min-height: calc(100vh - 124px)"
|
style="min-height: calc(100vh - 124px)"
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
class="list-list mx-2 md:mx-auto mt-8 rounded-xl border border-input-border p-4 max-w-full"
|
class="list-list mx-2 md:mx-auto rounded-xl border border-input-border p-4 max-w-full"
|
||||||
>
|
>
|
||||||
<button
|
<div class="flex gap-x-4 items-center">
|
||||||
class="flex w-full items-center gap-4 hover:bg-menu-item-background-hover transition-colors rounded-xl p-4 cursor-pointer"
|
<button
|
||||||
on:click={beforeListModalOpen}
|
class="flex w-full items-center gap-4 hover:bg-menu-item-background-hover transition-colors rounded-xl p-4 cursor-pointer"
|
||||||
>
|
on:click={beforeListModalOpen}
|
||||||
<i class="fa fa-plus text-xl aspect-square"></i>
|
>
|
||||||
<h5 id="create-list-button" class="text-xl font-semibold">
|
<i class="fa fa-plus text-xl aspect-square"></i>
|
||||||
{$_("create-new-list")}
|
<h5 id="create-list-button" class="text-xl font-semibold">
|
||||||
</h5>
|
{$_("create-new-list")}
|
||||||
</button>
|
</h5>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn-icon" on:click={toggleMap}
|
||||||
|
><i class="fa-regular fa-{showMap ? 'rectangle-list' : 'map'}"
|
||||||
|
></i></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr class="border-separator my-2" />
|
<hr class="border-separator my-2" />
|
||||||
{#each $lists as item, i}
|
{#each $lists as item, i}
|
||||||
<li class="list-list-item" on:click={() => list.set(item)} role="presentation">
|
<li
|
||||||
|
class="list-list-item"
|
||||||
|
on:click={() => setCurrentList(item)}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
<ListCard
|
<ListCard
|
||||||
list={item}
|
list={item}
|
||||||
on:change={(e) => handleDropdownClick(e, item)}
|
on:change={(e) => handleDropdownClick(e, item)}
|
||||||
@@ -97,11 +253,15 @@
|
|||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
<TrailList
|
<div id="map" class="rounded-xl z-0" class:hidden={!showMap}></div>
|
||||||
bind:filter
|
<div class:hidden={showMap}>
|
||||||
trails={$list.expand?.trails ?? []}
|
<TrailList
|
||||||
on:update={async () => await lists_index()}
|
bind:filter
|
||||||
></TrailList>
|
trails={$list.expand?.trails ?? []}
|
||||||
|
on:update={async () => await lists_index()}
|
||||||
|
></TrailList>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ListModal bind:openModal={openListModal} on:save={saveList}></ListModal>
|
<ListModal bind:openModal={openListModal} on:save={saveList}></ListModal>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
text={$_("delete-list-confirm")}
|
text={$_("delete-list-confirm")}
|
||||||
@@ -109,3 +269,9 @@
|
|||||||
on:confirm={deleteList}
|
on:confirm={deleteList}
|
||||||
></ConfirmModal>
|
></ConfirmModal>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#map {
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@
|
|||||||
const cityItems = response.results[1].hits.map(
|
const cityItems = response.results[1].hits.map(
|
||||||
(c: Record<string, any>) => ({
|
(c: Record<string, any>) => ({
|
||||||
text: c.name,
|
text: c.name,
|
||||||
description: `City | ${
|
description: `City | ${c.division} | ${
|
||||||
country_codes[
|
country_codes[
|
||||||
c["country code"] as keyof typeof country_codes
|
c["country code"] as keyof typeof country_codes
|
||||||
]
|
]
|
||||||
@@ -341,8 +341,4 @@
|
|||||||
height: calc(100vh - 124px);
|
height: calc(100vh - 124px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
:global(.leaflet-popup-content) {
|
|
||||||
width: max-content !important;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -51,8 +51,9 @@
|
|||||||
const result = await r.json();
|
const result = await r.json();
|
||||||
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
||||||
text: h.name,
|
text: h.name,
|
||||||
description:
|
description: `${h.division} | ${
|
||||||
country_codes[h["country code"] as keyof typeof country_codes],
|
country_codes[h["country code"] as keyof typeof country_codes]
|
||||||
|
}`,
|
||||||
value: h,
|
value: h,
|
||||||
icon: "city",
|
icon: "city",
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -186,7 +186,6 @@
|
|||||||
function addGPXLayer(gpx: string, addWaypoints: boolean = true) {
|
function addGPXLayer(gpx: string, addWaypoints: boolean = true) {
|
||||||
return new Promise<void>(function (resolve, reject) {
|
return new Promise<void>(function (resolve, reject) {
|
||||||
gpxLayer?.remove();
|
gpxLayer?.remove();
|
||||||
let startCoordinates: LatLng;
|
|
||||||
gpxLayer = new L.GPX(gpx, {
|
gpxLayer = new L.GPX(gpx, {
|
||||||
async: true,
|
async: true,
|
||||||
polyline_options: {
|
polyline_options: {
|
||||||
@@ -230,19 +229,8 @@
|
|||||||
.on("addpoint", function (e: any) {
|
.on("addpoint", function (e: any) {
|
||||||
if (e.point_type === "start") {
|
if (e.point_type === "start") {
|
||||||
e.point.setZIndexOffset(1000);
|
e.point.setZIndexOffset(1000);
|
||||||
startCoordinates = e.point._latlng;
|
$form.lat = e.point._latlng.lat;
|
||||||
} else if (e.point_type == "end") {
|
$form.lon = e.point._latlng.lng;
|
||||||
if (startCoordinates) {
|
|
||||||
$form.lat =
|
|
||||||
(startCoordinates.lat + e.point._latlng.lat) /
|
|
||||||
2;
|
|
||||||
$form.lon =
|
|
||||||
(startCoordinates.lng + e.point._latlng.lng) /
|
|
||||||
2;
|
|
||||||
} else {
|
|
||||||
$form.lat = e.point._latlng.lat;
|
|
||||||
$form.lon = e.point._latlng.lng;
|
|
||||||
}
|
|
||||||
} else if (e.point_type === "waypoint") {
|
} else if (e.point_type === "waypoint") {
|
||||||
const waypoint = new Waypoint(
|
const waypoint = new Waypoint(
|
||||||
e.point._latlng.lat,
|
e.point._latlng.lat,
|
||||||
|
|||||||
Reference in New Issue
Block a user