diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index e2ba426..b1b4441 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -7,6 +7,7 @@ from users.serializers import CustomUserDetailsSerializer from worldtravel.serializers import CountrySerializer, RegionSerializer, CitySerializer from geopy.distance import geodesic from integrations.models import ImmichIntegration +from adventures.utils.geojson import gpx_to_geojson import gpxpy import geojson import logging @@ -45,9 +46,10 @@ class ContentImageSerializer(CustomModelSerializer): class AttachmentSerializer(CustomModelSerializer): extension = serializers.SerializerMethodField() + geojson = serializers.SerializerMethodField() class Meta: model = ContentAttachment - fields = ['id', 'file', 'extension', 'name', 'user'] + fields = ['id', 'file', 'extension', 'name', 'user', 'geojson'] read_only_fields = ['id', 'user'] def get_extension(self, obj): @@ -62,6 +64,11 @@ class AttachmentSerializer(CustomModelSerializer): public_url = public_url.replace("'", "") representation['file'] = f"{public_url}/media/{instance.file.name}" return representation + + def get_geojson(self, obj): + if obj.file and obj.file.name.endswith('.gpx'): + return gpx_to_geojson(obj.file) + return None class CategorySerializer(serializers.ModelSerializer): num_locations = serializers.SerializerMethodField() @@ -186,28 +193,7 @@ class ActivitySerializer(CustomModelSerializer): return representation def get_geojson(self, obj): - if not obj.gpx_file: - return None - - try: - with obj.gpx_file.open('r') as f: - gpx = gpxpy.parse(f) - - features = [] - for track in gpx.tracks: - for segment in track.segments: - coords = [(pt.longitude, pt.latitude) for pt in segment.points] - if not coords: - continue - features.append(geojson.Feature( - geometry=geojson.LineString(coords), - properties={"name": track.name or "GPX Track"} - )) - - return geojson.FeatureCollection(features) - - except Exception as e: - return {"error": str(e)} + return gpx_to_geojson(obj.gpx_file) class VisitSerializer(serializers.ModelSerializer): diff --git a/backend/server/adventures/utils/geojson.py b/backend/server/adventures/utils/geojson.py new file mode 100644 index 0000000..2e8d04b --- /dev/null +++ b/backend/server/adventures/utils/geojson.py @@ -0,0 +1,39 @@ +import gpxpy +import geojson + +def gpx_to_geojson(gpx_file): + """ + Convert a GPX file to GeoJSON format. + + Args: + gpx_file: Django FileField or file-like object containing GPX data + + Returns: + dict: GeoJSON FeatureCollection or error dict + """ + if not gpx_file: + return None + + try: + with gpx_file.open('r') as f: + gpx = gpxpy.parse(f) + + features = [] + for track in gpx.tracks: + track_name = track.name or "GPX Track" + for segment in track.segments: + coords = [(point.longitude, point.latitude) for point in segment.points] + if coords: + feature = geojson.Feature( + geometry=geojson.LineString(coords), + properties={"name": track_name} + ) + features.append(feature) + + return geojson.FeatureCollection(features) + + except Exception as e: + return { + "error": str(e), + "message": "Failed to convert GPX to GeoJSON" + } \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 7426346..20e6036 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,7 +40,6 @@ "type": "module", "dependencies": { "@lukulent/svelte-umami": "^0.0.3", - "@mapbox/togeojson": "^0.16.2", "dompurify": "^3.2.4", "emoji-picker-element": "^1.26.0", "gsap": "^3.12.7", diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 87e5431..496edc8 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -278,6 +278,7 @@ export type Attachment = { extension: string; user: string; name: string; + geojson: any | null; // GeoJSON representation of the attachment if the file is a GPX }; export type Lodging = { @@ -417,7 +418,7 @@ export type Visit = { end_date: string; notes: string; timezone: string | null; - activities?: Activity[]; + activities: Activity[]; location: string; created_at: string; updated_at: string; diff --git a/frontend/src/routes/locations/[id]/+page.svelte b/frontend/src/routes/locations/[id]/+page.svelte index 0fbea59..3381460 100644 --- a/frontend/src/routes/locations/[id]/+page.svelte +++ b/frontend/src/routes/locations/[id]/+page.svelte @@ -9,8 +9,6 @@ import { marked } from 'marked'; import DOMPurify from 'dompurify'; // @ts-ignore - import toGeoJSON from '@mapbox/togeojson'; - // @ts-ignore import { DateTime } from 'luxon'; import LightbulbOn from '~icons/mdi/lightbulb-on'; @@ -29,95 +27,6 @@ return marked(markdown) as string; }; - async function getGpxFiles() { - let gpxfiles: string[] = []; - - if (adventure.attachments && adventure.attachments.length > 0) { - gpxfiles = adventure.attachments - .filter((attachment) => attachment.extension === 'gpx') - .map((attachment) => attachment.file); - } - - geojson = { - type: 'FeatureCollection', - features: [] - }; - - if (gpxfiles.length > 0) { - const promises = gpxfiles.map(async (gpxfile) => { - try { - const gpxFileName = gpxfile.split('/').pop(); - const res = await fetch(gpxfile, { - credentials: 'include' - }); - - if (!res.ok) { - console.error(`Failed to fetch GPX file: ${gpxFileName}`); - return []; - } - - const gpxData = await res.text(); - const parser = new DOMParser(); - const gpx = parser.parseFromString(gpxData, 'text/xml'); - - const convertedGeoJSON = toGeoJSON.gpx(gpx); - return convertedGeoJSON.features || []; - } catch (error) { - console.error(`Error processing GPX file ${gpxfile}:`, error); - return []; - } - }); - - const results = await Promise.allSettled(promises); - - results.forEach((result) => { - if (result.status === 'fulfilled' && result.value.length > 0) { - geojson.features.push(...result.value); - } - }); - } - - // for every location.visit there is an array of activitites which might have a field .geojson with a geojson object - if (adventure.visits && adventure.visits.length > 0) { - adventure.visits.forEach((visit) => { - if (visit.activities && visit.activities.length > 0) { - visit.activities.forEach((activity) => { - if (activity.geojson) { - // Ensure geojson object exists and has features array - if (!geojson || !geojson.features) { - geojson = { - type: 'FeatureCollection', - features: [] - }; - } - - // Extract features from the activity's FeatureCollection - if (activity.geojson.features && Array.isArray(activity.geojson.features)) { - activity.geojson.features.forEach((feature: { properties: any }) => { - // Add activity metadata to each feature's properties - const enhancedFeature = { - ...feature, - properties: { - ...feature.properties, - name: activity.name || 'Activity', - description: activity.type || '', - activityType: activity.type || 'unknown', - visitId: visit.id, - visitStartDate: visit.start_date, - visitEndDate: visit.end_date, - visitNotes: visit.notes || '' - } - }; - geojson.features.push(enhancedFeature); - }); - } - } - }); - } - }); - } - } - export let data: PageData; let measurementSystem = data.user?.measurement_system || 'metric'; console.log(data); @@ -150,13 +59,16 @@ } else { notFound = true; } - await getGpxFiles(); }); function hasActivityGeojson(adventure: AdditionalLocation) { return adventure.visits.some((visit) => visit.activities.some((activity) => activity.geojson)); } + function hasAttachmentGeojson(adventure: AdditionalLocation) { + return adventure.attachments.some((attachment) => attachment.geojson); + } + function getActivityColor(activityType: string) { const colors: Record = { Hike: '#10B981', @@ -204,13 +116,6 @@ return measurementSystem === 'imperial' ? totalMeters * 3.28084 : totalMeters; } - async function saveEdit(event: CustomEvent) { - adventure = event.detail; - isEditModalOpen = false; - geojson = null; - await getGpxFiles(); - } - function closeImageModal() { isImageModalOpen = false; } @@ -616,7 +521,7 @@ {/if} - {#if (adventure.longitude && adventure.latitude) || geojson || hasActivityGeojson(adventure)} + {#if (adventure.longitude && adventure.latitude) || hasAttachmentGeojson(adventure) || hasActivityGeojson(adventure)}

🗺️ {$t('adventures.location')}

@@ -794,6 +699,20 @@ {/each} {/each} + {#each adventure.attachments as attachment} + {#if attachment.geojson} + + + + {/if} + {/each} + {#if adventure.longitude && adventure.latitude}