Merge pull request #189 from M-Enderle/feat/gpx-full-route-import
feat(add-gpx-tracks): adds better gpx track views
This commit is contained in:
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "2.7.0",
|
"version": "2.7.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "2.7.0",
|
"version": "2.7.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
|||||||
@@ -508,6 +508,24 @@ export function MapView({
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* GPX imported route geometries */}
|
||||||
|
{places.map((place) => {
|
||||||
|
if (!place.route_geometry) return null
|
||||||
|
try {
|
||||||
|
const coords = JSON.parse(place.route_geometry) as [number, number][]
|
||||||
|
if (!coords || coords.length < 2) return null
|
||||||
|
return (
|
||||||
|
<Polyline
|
||||||
|
key={`gpx-${place.id}`}
|
||||||
|
positions={coords}
|
||||||
|
color={place.category_color || '#3b82f6'}
|
||||||
|
weight={3.5}
|
||||||
|
opacity={0.75}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} catch { return null }
|
||||||
|
})}
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
||||||
import { X, Clock, MapPin, ExternalLink, Phone, Euro, Edit2, Trash2, Plus, Minus, ChevronDown, ChevronUp, FileText, Upload, File, FileImage, Star, Navigation, Users } from 'lucide-react'
|
import { X, Clock, MapPin, ExternalLink, Phone, Euro, Edit2, Trash2, Plus, Minus, ChevronDown, ChevronUp, FileText, Upload, File, FileImage, Star, Navigation, Users, Mountain, TrendingUp } from 'lucide-react'
|
||||||
import PlaceAvatar from '../shared/PlaceAvatar'
|
import PlaceAvatar from '../shared/PlaceAvatar'
|
||||||
import { mapsApi } from '../../api/client'
|
import { mapsApi } from '../../api/client'
|
||||||
import { useSettingsStore } from '../../store/settingsStore'
|
import { useSettingsStore } from '../../store/settingsStore'
|
||||||
@@ -461,6 +461,98 @@ export default function PlaceInspector({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{/* GPX Track stats */}
|
||||||
|
{place.route_geometry && (() => {
|
||||||
|
try {
|
||||||
|
const pts: number[][] = JSON.parse(place.route_geometry)
|
||||||
|
if (!pts || pts.length < 2) return null
|
||||||
|
const hasEle = pts[0].length >= 3
|
||||||
|
|
||||||
|
// Haversine distance
|
||||||
|
const toRad = (d: number) => d * Math.PI / 180
|
||||||
|
let totalDist = 0
|
||||||
|
for (let i = 1; i < pts.length; i++) {
|
||||||
|
const [lat1, lng1] = pts[i - 1], [lat2, lng2] = pts[i]
|
||||||
|
const dLat = toRad(lat2 - lat1), dLng = toRad(lng2 - lng1)
|
||||||
|
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2
|
||||||
|
totalDist += 6371000 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||||||
|
}
|
||||||
|
const distKm = totalDist / 1000
|
||||||
|
|
||||||
|
// Elevation stats
|
||||||
|
let minEle = Infinity, maxEle = -Infinity, totalUp = 0, totalDown = 0
|
||||||
|
if (hasEle) {
|
||||||
|
for (let i = 0; i < pts.length; i++) {
|
||||||
|
const e = pts[i][2]
|
||||||
|
if (e < minEle) minEle = e
|
||||||
|
if (e > maxEle) maxEle = e
|
||||||
|
if (i > 0) {
|
||||||
|
const diff = e - pts[i - 1][2]
|
||||||
|
if (diff > 0) totalUp += diff; else totalDown += Math.abs(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elevation profile SVG
|
||||||
|
const chartW = 280, chartH = 60
|
||||||
|
const elevations = hasEle ? pts.map(p => p[2]) : []
|
||||||
|
let pathD = ''
|
||||||
|
if (elevations.length > 1) {
|
||||||
|
const step = Math.max(1, Math.floor(elevations.length / chartW))
|
||||||
|
const sampled = elevations.filter((_, i) => i % step === 0)
|
||||||
|
const eMin = Math.min(...sampled), eMax = Math.max(...sampled)
|
||||||
|
const range = eMax - eMin || 1
|
||||||
|
pathD = sampled.map((e, i) => {
|
||||||
|
const x = (i / (sampled.length - 1)) * chartW
|
||||||
|
const y = chartH - ((e - eMin) / range) * (chartH - 4) - 2
|
||||||
|
return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`
|
||||||
|
}).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ background: 'var(--bg-hover)', borderRadius: 10, padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<TrendingUp size={13} color="#9ca3af" />
|
||||||
|
<span style={{ fontSize: 12, color: 'var(--text-secondary)', fontWeight: 500 }}>{t('inspector.trackStats')}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, color: 'var(--text-primary)', fontWeight: 600 }}>
|
||||||
|
<MapPin size={12} color="#3b82f6" />
|
||||||
|
{distKm < 1 ? `${Math.round(totalDist)} m` : `${distKm.toFixed(1)} km`}
|
||||||
|
</div>
|
||||||
|
{hasEle && (
|
||||||
|
<>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, color: 'var(--text-primary)', fontWeight: 600 }}>
|
||||||
|
<Mountain size={12} color="#22c55e" />
|
||||||
|
{Math.round(maxEle)} m
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, color: 'var(--text-primary)', fontWeight: 600 }}>
|
||||||
|
<Mountain size={12} color="#ef4444" />
|
||||||
|
{Math.round(minEle)} m
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
||||||
|
↑{Math.round(totalUp)} m ↓{Math.round(totalDown)} m
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{pathD && (
|
||||||
|
<svg width="100%" viewBox={`0 0 ${chartW} ${chartH}`} preserveAspectRatio="none" style={{ display: 'block', borderRadius: 6, background: 'var(--bg-tertiary)' }}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id={`ele-grad-${place.id}`} x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.25" />
|
||||||
|
<stop offset="100%" stopColor="#3b82f6" stopOpacity="0.02" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path d={`${pathD} L${chartW},${chartH} L0,${chartH} Z`} fill={`url(#ele-grad-${place.id})`} />
|
||||||
|
<path d={pathD} fill="none" stroke="#3b82f6" strokeWidth="1.5" vectorEffect="non-scaling-stroke" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} catch { return null }
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Files section */}
|
{/* Files section */}
|
||||||
{(placeFiles.length > 0 || onFileUpload) && (
|
{(placeFiles.length > 0 || onFileUpload) && (
|
||||||
<div style={{ background: 'var(--bg-hover)', borderRadius: 10, overflow: 'hidden' }}>
|
<div style={{ background: 'var(--bg-hover)', borderRadius: 10, overflow: 'hidden' }}>
|
||||||
|
|||||||
@@ -815,6 +815,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'inspector.addRes': 'حجز',
|
'inspector.addRes': 'حجز',
|
||||||
'inspector.editRes': 'تعديل الحجز',
|
'inspector.editRes': 'تعديل الحجز',
|
||||||
'inspector.participants': 'المشاركون',
|
'inspector.participants': 'المشاركون',
|
||||||
|
'inspector.trackStats': 'بيانات المسار',
|
||||||
|
|
||||||
// Reservations
|
// Reservations
|
||||||
'reservations.title': 'الحجوزات',
|
'reservations.title': 'الحجوزات',
|
||||||
|
|||||||
@@ -794,6 +794,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'inspector.addRes': 'Reserva',
|
'inspector.addRes': 'Reserva',
|
||||||
'inspector.editRes': 'Editar reserva',
|
'inspector.editRes': 'Editar reserva',
|
||||||
'inspector.participants': 'Participantes',
|
'inspector.participants': 'Participantes',
|
||||||
|
'inspector.trackStats': 'Dados da trilha',
|
||||||
|
|
||||||
// Reservations
|
// Reservations
|
||||||
'reservations.title': 'Reservas',
|
'reservations.title': 'Reservas',
|
||||||
|
|||||||
@@ -816,6 +816,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'inspector.addRes': 'Rezervace',
|
'inspector.addRes': 'Rezervace',
|
||||||
'inspector.editRes': 'Upravit rezervaci',
|
'inspector.editRes': 'Upravit rezervaci',
|
||||||
'inspector.participants': 'Účastníci',
|
'inspector.participants': 'Účastníci',
|
||||||
|
'inspector.trackStats': 'Data trasy',
|
||||||
|
|
||||||
// Rezervace (Reservations)
|
// Rezervace (Reservations)
|
||||||
'reservations.title': 'Rezervace',
|
'reservations.title': 'Rezervace',
|
||||||
|
|||||||
@@ -812,6 +812,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'inspector.addRes': 'Reservierung',
|
'inspector.addRes': 'Reservierung',
|
||||||
'inspector.editRes': 'Reservierung bearbeiten',
|
'inspector.editRes': 'Reservierung bearbeiten',
|
||||||
'inspector.participants': 'Teilnehmer',
|
'inspector.participants': 'Teilnehmer',
|
||||||
|
'inspector.trackStats': 'Streckendaten',
|
||||||
|
|
||||||
// Reservations
|
// Reservations
|
||||||
'reservations.title': 'Buchungen',
|
'reservations.title': 'Buchungen',
|
||||||
|
|||||||
@@ -809,6 +809,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'inspector.addRes': 'Reservation',
|
'inspector.addRes': 'Reservation',
|
||||||
'inspector.editRes': 'Edit Reservation',
|
'inspector.editRes': 'Edit Reservation',
|
||||||
'inspector.participants': 'Participants',
|
'inspector.participants': 'Participants',
|
||||||
|
'inspector.trackStats': 'Track Stats',
|
||||||
|
|
||||||
// Reservations
|
// Reservations
|
||||||
'reservations.title': 'Bookings',
|
'reservations.title': 'Bookings',
|
||||||
|
|||||||
@@ -789,6 +789,7 @@ const es: Record<string, string> = {
|
|||||||
'inspector.addRes': 'Reserva',
|
'inspector.addRes': 'Reserva',
|
||||||
'inspector.editRes': 'Editar reserva',
|
'inspector.editRes': 'Editar reserva',
|
||||||
'inspector.participants': 'Participantes',
|
'inspector.participants': 'Participantes',
|
||||||
|
'inspector.trackStats': 'Datos de la ruta',
|
||||||
|
|
||||||
// Reservations
|
// Reservations
|
||||||
'reservations.title': 'Reservas',
|
'reservations.title': 'Reservas',
|
||||||
|
|||||||
@@ -811,6 +811,7 @@ const fr: Record<string, string> = {
|
|||||||
'inspector.addRes': 'Réservation',
|
'inspector.addRes': 'Réservation',
|
||||||
'inspector.editRes': 'Modifier la réservation',
|
'inspector.editRes': 'Modifier la réservation',
|
||||||
'inspector.participants': 'Participants',
|
'inspector.participants': 'Participants',
|
||||||
|
'inspector.trackStats': 'Données du parcours',
|
||||||
|
|
||||||
// Reservations
|
// Reservations
|
||||||
'reservations.title': 'Réservations',
|
'reservations.title': 'Réservations',
|
||||||
|
|||||||
@@ -810,6 +810,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'inspector.addRes': 'Foglalás',
|
'inspector.addRes': 'Foglalás',
|
||||||
'inspector.editRes': 'Foglalás szerkesztése',
|
'inspector.editRes': 'Foglalás szerkesztése',
|
||||||
'inspector.participants': 'Résztvevők',
|
'inspector.participants': 'Résztvevők',
|
||||||
|
'inspector.trackStats': 'Útvonal adatok',
|
||||||
|
|
||||||
// Foglalások
|
// Foglalások
|
||||||
'reservations.title': 'Foglalások',
|
'reservations.title': 'Foglalások',
|
||||||
|
|||||||
@@ -811,6 +811,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'inspector.addRes': 'Prenotazione',
|
'inspector.addRes': 'Prenotazione',
|
||||||
'inspector.editRes': 'Modifica prenotazione',
|
'inspector.editRes': 'Modifica prenotazione',
|
||||||
'inspector.participants': 'Partecipanti',
|
'inspector.participants': 'Partecipanti',
|
||||||
|
'inspector.trackStats': 'Dati del percorso',
|
||||||
|
|
||||||
// Reservations
|
// Reservations
|
||||||
'reservations.title': 'Prenotazioni',
|
'reservations.title': 'Prenotazioni',
|
||||||
|
|||||||
@@ -811,6 +811,7 @@ const nl: Record<string, string> = {
|
|||||||
'inspector.addRes': 'Reservering',
|
'inspector.addRes': 'Reservering',
|
||||||
'inspector.editRes': 'Reservering bewerken',
|
'inspector.editRes': 'Reservering bewerken',
|
||||||
'inspector.participants': 'Deelnemers',
|
'inspector.participants': 'Deelnemers',
|
||||||
|
'inspector.trackStats': 'Routegegevens',
|
||||||
|
|
||||||
// Reservations
|
// Reservations
|
||||||
'reservations.title': 'Boekingen',
|
'reservations.title': 'Boekingen',
|
||||||
|
|||||||
@@ -811,6 +811,7 @@ const ru: Record<string, string> = {
|
|||||||
'inspector.addRes': 'Бронирование',
|
'inspector.addRes': 'Бронирование',
|
||||||
'inspector.editRes': 'Редактировать бронирование',
|
'inspector.editRes': 'Редактировать бронирование',
|
||||||
'inspector.participants': 'Участники',
|
'inspector.participants': 'Участники',
|
||||||
|
'inspector.trackStats': 'Данные маршрута',
|
||||||
|
|
||||||
// Reservations
|
// Reservations
|
||||||
'reservations.title': 'Бронирования',
|
'reservations.title': 'Бронирования',
|
||||||
|
|||||||
@@ -811,6 +811,7 @@ const zh: Record<string, string> = {
|
|||||||
'inspector.addRes': '预订',
|
'inspector.addRes': '预订',
|
||||||
'inspector.editRes': '编辑预订',
|
'inspector.editRes': '编辑预订',
|
||||||
'inspector.participants': '参与者',
|
'inspector.participants': '参与者',
|
||||||
|
'inspector.trackStats': '轨迹数据',
|
||||||
|
|
||||||
// Reservations
|
// Reservations
|
||||||
'reservations.title': '预订',
|
'reservations.title': '预订',
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export interface Place {
|
|||||||
image_url: string | null
|
image_url: string | null
|
||||||
google_place_id: string | null
|
google_place_id: string | null
|
||||||
osm_id: string | null
|
osm_id: string | null
|
||||||
|
route_geometry: string | null
|
||||||
place_time: string | null
|
place_time: string | null
|
||||||
end_time: string | null
|
end_time: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
|
|||||||
@@ -427,6 +427,10 @@ function runMigrations(db: Database.Database): void {
|
|||||||
db.prepare("UPDATE addons SET type = 'integration' WHERE id = 'mcp'").run();
|
db.prepare("UPDATE addons SET type = 'integration' WHERE id = 'mcp'").run();
|
||||||
} catch {}
|
} catch {}
|
||||||
},
|
},
|
||||||
|
() => {
|
||||||
|
// GPX full route geometry stored as JSON array of [lat,lng] pairs
|
||||||
|
try { db.exec('ALTER TABLE places ADD COLUMN route_geometry TEXT'); } catch {}
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (currentVersion < migrations.length) {
|
if (currentVersion < migrations.length) {
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ router.post('/', authenticate, requireTripAccess, validateStringLengths({ name:
|
|||||||
broadcast(tripId, 'place:created', { place }, req.headers['x-socket-id'] as string);
|
broadcast(tripId, 'place:created', { place }, req.headers['x-socket-id'] as string);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Import places from GPX file (must be before /:id)
|
// Import places from GPX file with full track geometry (must be before /:id)
|
||||||
router.post('/import/gpx', authenticate, requireTripAccess, gpxUpload.single('file'), (req: Request, res: Response) => {
|
router.post('/import/gpx', authenticate, requireTripAccess, gpxUpload.single('file'), (req: Request, res: Response) => {
|
||||||
const { tripId } = req.params;
|
const { tripId } = req.params;
|
||||||
const file = (req as any).file;
|
const file = (req as any).file;
|
||||||
@@ -136,7 +136,7 @@ router.post('/import/gpx', authenticate, requireTripAccess, gpxUpload.single('fi
|
|||||||
const extractName = (body: string) => { const m = body.match(/<name[^>]*>([\s\S]*?)<\/name>/i); return m ? stripCdata(m[1]) : null };
|
const extractName = (body: string) => { const m = body.match(/<name[^>]*>([\s\S]*?)<\/name>/i); return m ? stripCdata(m[1]) : null };
|
||||||
const extractDesc = (body: string) => { const m = body.match(/<desc[^>]*>([\s\S]*?)<\/desc>/i); return m ? stripCdata(m[1]) : null };
|
const extractDesc = (body: string) => { const m = body.match(/<desc[^>]*>([\s\S]*?)<\/desc>/i); return m ? stripCdata(m[1]) : null };
|
||||||
|
|
||||||
const waypoints: { name: string; lat: number; lng: number; description: string | null }[] = [];
|
const waypoints: { name: string; lat: number; lng: number; description: string | null; routeGeometry?: string }[] = [];
|
||||||
|
|
||||||
// 1) Parse <wpt> elements (named waypoints / POIs)
|
// 1) Parse <wpt> elements (named waypoints / POIs)
|
||||||
const wptRegex = /<wpt\s([^>]+)>([\s\S]*?)<\/wpt>/gi;
|
const wptRegex = /<wpt\s([^>]+)>([\s\S]*?)<\/wpt>/gi;
|
||||||
@@ -159,23 +159,25 @@ router.post('/import/gpx', authenticate, requireTripAccess, gpxUpload.single('fi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) If still nothing, extract track name + start/end points from <trkpt>
|
// 3) If still nothing, extract full track geometry from <trkpt>
|
||||||
if (waypoints.length === 0) {
|
if (waypoints.length === 0) {
|
||||||
const trackNameMatch = xml.match(/<trk[^>]*>[\s\S]*?<name[^>]*>([\s\S]*?)<\/name>/i);
|
const trackNameMatch = xml.match(/<trk[^>]*>[\s\S]*?<name[^>]*>([\s\S]*?)<\/name>/i);
|
||||||
const trackName = trackNameMatch?.[1]?.trim() || 'GPX Track';
|
const trackName = trackNameMatch?.[1]?.trim() || 'GPX Track';
|
||||||
|
const trackDesc = (() => { const m = xml.match(/<trk[^>]*>[\s\S]*?<desc[^>]*>([\s\S]*?)<\/desc>/i); return m ? stripCdata(m[1]) : null })();
|
||||||
const trkptRegex = /<trkpt\s([^>]*?)(?:\/>|>([\s\S]*?)<\/trkpt>)/gi;
|
const trkptRegex = /<trkpt\s([^>]*?)(?:\/>|>([\s\S]*?)<\/trkpt>)/gi;
|
||||||
const trackPoints: { lat: number; lng: number }[] = [];
|
const trackPoints: { lat: number; lng: number; ele: number | null }[] = [];
|
||||||
while ((match = trkptRegex.exec(xml)) !== null) {
|
while ((match = trkptRegex.exec(xml)) !== null) {
|
||||||
const coords = parseCoords(match[1]);
|
const coords = parseCoords(match[1]);
|
||||||
if (coords) trackPoints.push(coords);
|
if (!coords) continue;
|
||||||
|
const eleMatch = match[2]?.match(/<ele[^>]*>([\s\S]*?)<\/ele>/i);
|
||||||
|
const ele = eleMatch ? parseFloat(eleMatch[1]) : null;
|
||||||
|
trackPoints.push({ ...coords, ele: (ele !== null && !isNaN(ele)) ? ele : null });
|
||||||
}
|
}
|
||||||
if (trackPoints.length > 0) {
|
if (trackPoints.length > 0) {
|
||||||
const start = trackPoints[0];
|
const start = trackPoints[0];
|
||||||
waypoints.push({ ...start, name: `${trackName} — Start`, description: null });
|
const hasAllEle = trackPoints.every(p => p.ele !== null);
|
||||||
if (trackPoints.length > 1) {
|
const routeGeometry = trackPoints.map(p => hasAllEle ? [p.lat, p.lng, p.ele] : [p.lat, p.lng]);
|
||||||
const end = trackPoints[trackPoints.length - 1];
|
waypoints.push({ ...start, name: trackName, description: trackDesc, routeGeometry: JSON.stringify(routeGeometry) });
|
||||||
waypoints.push({ ...end, name: `${trackName} — End`, description: null });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,13 +186,13 @@ router.post('/import/gpx', authenticate, requireTripAccess, gpxUpload.single('fi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const insertStmt = db.prepare(`
|
const insertStmt = db.prepare(`
|
||||||
INSERT INTO places (trip_id, name, description, lat, lng, transport_mode)
|
INSERT INTO places (trip_id, name, description, lat, lng, transport_mode, route_geometry)
|
||||||
VALUES (?, ?, ?, ?, ?, 'walking')
|
VALUES (?, ?, ?, ?, ?, 'walking', ?)
|
||||||
`);
|
`);
|
||||||
const created: any[] = [];
|
const created: any[] = [];
|
||||||
const insertAll = db.transaction(() => {
|
const insertAll = db.transaction(() => {
|
||||||
for (const wp of waypoints) {
|
for (const wp of waypoints) {
|
||||||
const result = insertStmt.run(tripId, wp.name, wp.description, wp.lat, wp.lng);
|
const result = insertStmt.run(tripId, wp.name, wp.description, wp.lat, wp.lng, wp.routeGeometry || null);
|
||||||
const place = getPlaceWithTags(Number(result.lastInsertRowid));
|
const place = getPlaceWithTags(Number(result.lastInsertRowid));
|
||||||
created.push(place);
|
created.push(place);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user