1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-08 14:45:17 +02:00

feat: add geojson support for attachments and refactor GPX handling in location page

This commit is contained in:
Sean Morley 2025-08-07 20:14:50 -04:00
parent b33587e516
commit de53456bc7
5 changed files with 69 additions and 125 deletions

View file

@ -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):

View file

@ -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"
}

View file

@ -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",

View file

@ -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;

View file

@ -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<string, string> = {
Hike: '#10B981',
@ -204,13 +116,6 @@
return measurementSystem === 'imperial' ? totalMeters * 3.28084 : totalMeters;
}
async function saveEdit(event: CustomEvent<AdditionalLocation>) {
adventure = event.detail;
isEditModalOpen = false;
geojson = null;
await getGpxFiles();
}
function closeImageModal() {
isImageModalOpen = false;
}
@ -616,7 +521,7 @@
{/if}
<!-- Map Section -->
{#if (adventure.longitude && adventure.latitude) || geojson || hasActivityGeojson(adventure)}
{#if (adventure.longitude && adventure.latitude) || hasAttachmentGeojson(adventure) || hasActivityGeojson(adventure)}
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">🗺️ {$t('adventures.location')}</h2>
@ -794,6 +699,20 @@
{/each}
{/each}
{#each adventure.attachments as attachment}
{#if attachment.geojson}
<GeoJSON data={attachment.geojson}>
<LineLayer
paint={{
'line-color': '#00FF00',
'line-width': 2,
'line-opacity': 0.6
}}
/>
</GeoJSON>
{/if}
{/each}
{#if adventure.longitude && adventure.latitude}
<DefaultMarker lngLat={{ lng: adventure.longitude, lat: adventure.latitude }}>
<Popup openOn="click" offset={[0, -10]}>