diff --git a/backend/server/adventures/views/attachment_view.py b/backend/server/adventures/views/attachment_view.py index 47ed328..e83bdea 100644 --- a/backend/server/adventures/views/attachment_view.py +++ b/backend/server/adventures/views/attachment_view.py @@ -2,10 +2,8 @@ from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from django.db.models import Q from adventures.models import Adventure, Attachment from adventures.serializers import AttachmentSerializer -import uuid class AttachmentViewSet(viewsets.ModelViewSet): serializer_class = AttachmentSerializer diff --git a/frontend/package.json b/frontend/package.json index f8abe55..5b53d58 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,6 +40,7 @@ "type": "module", "dependencies": { "@lukulent/svelte-umami": "^0.0.3", + "@mapbox/togeojson": "^0.16.2", "emoji-picker-element": "^1.26.0", "gsap": "^3.12.7", "marked": "^15.0.4", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3e72f67..7a021cb 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@lukulent/svelte-umami': specifier: ^0.0.3 version: 0.0.3(svelte@4.2.19) + '@mapbox/togeojson': + specifier: ^0.16.2 + version: 0.16.2 emoji-picker-element: specifier: ^1.26.0 version: 1.26.0 @@ -483,6 +486,10 @@ packages: '@mapbox/tiny-sdf@2.0.6': resolution: {integrity: sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==} + '@mapbox/togeojson@0.16.2': + resolution: {integrity: sha512-DcApudmw4g/grOrpM5gYPZfts6Kr8litBESN6n/27sDsjR2f+iJhx4BA0J2B+XrLlnHyJkKztYApe6oCUZpzFA==} + hasBin: true + '@mapbox/unitbezier@0.0.1': resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} @@ -868,6 +875,10 @@ packages: engines: {node: '>=16'} hasBin: true + '@xmldom/xmldom@0.8.10': + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -972,6 +983,9 @@ packages: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} @@ -1039,6 +1053,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + confbox@0.1.7: resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} @@ -2116,6 +2134,9 @@ packages: type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typescript@5.5.2: resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} engines: {node: '>=14.17'} @@ -2553,6 +2574,12 @@ snapshots: '@mapbox/tiny-sdf@2.0.6': {} + '@mapbox/togeojson@0.16.2': + dependencies: + '@xmldom/xmldom': 0.8.10 + concat-stream: 2.0.0 + minimist: 1.2.8 + '@mapbox/unitbezier@0.0.1': {} '@mapbox/vector-tile@1.3.1': @@ -3031,6 +3058,8 @@ snapshots: - encoding - supports-color + '@xmldom/xmldom@0.8.10': {} + abbrev@1.1.1: {} acorn-import-attributes@1.9.5(acorn@8.12.0): @@ -3125,6 +3154,8 @@ snapshots: buffer-crc32@1.0.0: {} + buffer-from@1.1.2: {} + builtin-modules@3.3.0: {} bytewise-core@1.2.3: @@ -3196,6 +3227,13 @@ snapshots: concat-map@0.0.1: {} + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + confbox@0.1.7: {} console-control-strings@1.1.0: {} @@ -4320,6 +4358,8 @@ snapshots: type@2.7.3: {} + typedarray@0.0.6: {} + typescript@5.5.2: {} typewise-core@1.2.0: {} diff --git a/frontend/src/lib/components/AttachmentCard.svelte b/frontend/src/lib/components/AttachmentCard.svelte index 1de97e8..226b052 100644 --- a/frontend/src/lib/components/AttachmentCard.svelte +++ b/frontend/src/lib/components/AttachmentCard.svelte @@ -27,7 +27,7 @@ const isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].some((ext) => attachment.file.endsWith(ext) ); - return isImage ? `url(${attachment.file})` : 'url(/path/to/default-placeholder.png)'; + return isImage ? `url(${attachment.file})` : ''; } diff --git a/frontend/src/routes/adventures/[id]/+page.svelte b/frontend/src/routes/adventures/[id]/+page.svelte index a471572..90fc283 100644 --- a/frontend/src/routes/adventures/[id]/+page.svelte +++ b/frontend/src/routes/adventures/[id]/+page.svelte @@ -4,14 +4,65 @@ import type { PageData } from './$types'; import { goto } from '$app/navigation'; import Lost from '$lib/assets/undraw_lost.svg'; - import { DefaultMarker, MapLibre, Popup } from 'svelte-maplibre'; + import { DefaultMarker, MapLibre, Popup, GeoJSON, LineLayer } from 'svelte-maplibre'; import { t } from 'svelte-i18n'; import { marked } from 'marked'; // Import the markdown parser + // @ts-ignore + import toGeoJSON from '@mapbox/togeojson'; + + let geojson: any; + const renderMarkdown = (markdown: string) => { return marked(markdown); }; + async function getGpxFiles() { + let gpxfiles: string[] = []; + + // Collect all GPX file attachments + if (adventure.attachments && adventure.attachments.length > 0) { + adventure.attachments + .filter((attachment) => attachment.extension === 'gpx') + .forEach((attachment) => gpxfiles.push(attachment.file)); + } + + // Initialize a collection GeoJSON object + geojson = { + type: 'FeatureCollection', + features: [] + }; + + // Process each GPX file + if (gpxfiles.length > 0) { + for (const gpxfile of gpxfiles) { + try { + let gpxFileName = gpxfile.split('/').pop(); + let res = await fetch('/gpx/' + gpxFileName); + + if (!res.ok) { + console.error(`Failed to fetch GPX file: ${gpxFileName}`); + continue; + } + + let gpxData = await res.text(); + let parser = new DOMParser(); + let gpx = parser.parseFromString(gpxData, 'text/xml'); + + // Convert GPX to GeoJSON and merge features + let convertedGeoJSON = toGeoJSON.gpx(gpx); + if (convertedGeoJSON.features) { + geojson.features.push(...convertedGeoJSON.features); + } + } catch (error) { + console.error(`Error processing GPX file ${gpxfile}:`, error); + } + } + + // Log the final GeoJSON for debugging + } + } + export let data: PageData; console.log(data); @@ -32,7 +83,7 @@ import ImageDisplayModal from '$lib/components/ImageDisplayModal.svelte'; import AttachmentCard from '$lib/components/AttachmentCard.svelte'; - onMount(() => { + onMount(async () => { if (data.props.adventure) { adventure = data.props.adventure; // sort so that any image in adventure_images .is_primary is first @@ -48,6 +99,7 @@ } else { notFound = true; } + getGpxFiles(); }); function saveEdit(event: CustomEvent) { @@ -345,6 +397,19 @@ center={{ lng: adventure.longitude, lat: adventure.latitude }} zoom={12} > + + {#if geojson} + + + + + {/if} + diff --git a/frontend/src/routes/gpx/[file]/+server.ts b/frontend/src/routes/gpx/[file]/+server.ts new file mode 100644 index 0000000..9edaa38 --- /dev/null +++ b/frontend/src/routes/gpx/[file]/+server.ts @@ -0,0 +1,22 @@ +const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; +const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; + +/** @type {import('./$types').RequestHandler} */ +export async function GET(event) { + let sessionid = event.cookies.get('sessionid'); + let fileName = event.params.file; + let res = await fetch(`${endpoint}/media/attachments/${fileName}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Cookie: `sessionid=${sessionid}` + } + }); + let data = await res.text(); + return new Response(data, { + status: res.status, + headers: { + 'Content-Type': 'application/xml' + } + }); +}