fix: keep itinerary connectors visible when route data is unavailable

This commit is contained in:
2026-03-07 15:28:33 +00:00
parent cf84feb783
commit eb612f1cdf

View File

@@ -31,6 +31,7 @@
import ItineraryLinkModal from '$lib/components/collections/ItineraryLinkModal.svelte'; import ItineraryLinkModal from '$lib/components/collections/ItineraryLinkModal.svelte';
import ItineraryDayPickModal from '$lib/components/collections/ItineraryDayPickModal.svelte'; import ItineraryDayPickModal from '$lib/components/collections/ItineraryDayPickModal.svelte';
import Car from '~icons/mdi/car'; import Car from '~icons/mdi/car';
import Walk from '~icons/mdi/walk';
import LocationMarker from '~icons/mdi/map-marker'; import LocationMarker from '~icons/mdi/map-marker';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { addToast } from '$lib/toasts'; import { addToast } from '$lib/toasts';
@@ -430,30 +431,42 @@
return (degrees * Math.PI) / 180; return (degrees * Math.PI) / 180;
} }
function haversineDistanceKm(from: Location, to: Location): number | null { function normalizeCoordinate(value: number | string | null | undefined): number | null {
if ( if (typeof value === 'number') {
from.latitude === null || return Number.isFinite(value) ? value : null;
from.longitude === null ||
to.latitude === null ||
to.longitude === null
) {
return null;
} }
if (typeof value === 'string') {
const trimmed = value.trim();
if (!trimmed) return null;
const parsed = Number(trimmed);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function haversineDistanceKm(from: Location, to: Location): number | null {
const fromLatitude = normalizeCoordinate(from.latitude);
const fromLongitude = normalizeCoordinate(from.longitude);
const toLatitude = normalizeCoordinate(to.latitude);
const toLongitude = normalizeCoordinate(to.longitude);
if ( if (
!Number.isFinite(from.latitude) || fromLatitude === null ||
!Number.isFinite(from.longitude) || fromLongitude === null ||
!Number.isFinite(to.latitude) || toLatitude === null ||
!Number.isFinite(to.longitude) toLongitude === null
) { ) {
return null; return null;
} }
const earthRadiusKm = 6371; const earthRadiusKm = 6371;
const latDelta = toRadians(to.latitude - from.latitude); const latDelta = toRadians(toLatitude - fromLatitude);
const lonDelta = toRadians(to.longitude - from.longitude); const lonDelta = toRadians(toLongitude - fromLongitude);
const fromLat = toRadians(from.latitude); const fromLat = toRadians(fromLatitude);
const toLat = toRadians(to.latitude); const toLat = toRadians(toLatitude);
const a = const a =
Math.sin(latDelta / 2) * Math.sin(latDelta / 2) + Math.sin(latDelta / 2) * Math.sin(latDelta / 2) +
@@ -478,6 +491,7 @@
distanceLabel: string; distanceLabel: string;
durationLabel: string; durationLabel: string;
mode: 'walking' | 'driving'; mode: 'walking' | 'driving';
unavailable?: boolean;
}; };
type ConnectorPair = { type ConnectorPair = {
@@ -526,20 +540,16 @@
const nextLocation = nextItem.resolvedObject as Location | null; const nextLocation = nextItem.resolvedObject as Location | null;
if (!currentLocation || !nextLocation) return null; if (!currentLocation || !nextLocation) return null;
const fromLatitude = currentLocation.latitude; const fromLatitude = normalizeCoordinate(currentLocation.latitude);
const fromLongitude = currentLocation.longitude; const fromLongitude = normalizeCoordinate(currentLocation.longitude);
const toLatitude = nextLocation.latitude; const toLatitude = normalizeCoordinate(nextLocation.latitude);
const toLongitude = nextLocation.longitude; const toLongitude = normalizeCoordinate(nextLocation.longitude);
if ( if (
fromLatitude === null || fromLatitude === null ||
fromLongitude === null || fromLongitude === null ||
toLatitude === null || toLatitude === null ||
toLongitude === null || toLongitude === null
!Number.isFinite(fromLatitude) ||
!Number.isFinite(fromLongitude) ||
!Number.isFinite(toLatitude) ||
!Number.isFinite(toLongitude)
) { ) {
return null; return null;
} }
@@ -707,12 +717,19 @@
const nextType = nextItem.item?.type || ''; const nextType = nextItem.item?.type || '';
if (currentType !== 'location' || nextType !== 'location') return null; if (currentType !== 'location' || nextType !== 'location') return null;
const unavailableConnector: LocationConnector = {
distanceLabel: '',
durationLabel: getI18nText('itinerary.route_unavailable', 'Route unavailable'),
mode: 'walking',
unavailable: true
};
const currentLocation = currentItem.resolvedObject as Location | null; const currentLocation = currentItem.resolvedObject as Location | null;
const nextLocation = nextItem.resolvedObject as Location | null; const nextLocation = nextItem.resolvedObject as Location | null;
if (!currentLocation || !nextLocation) return null; if (!currentLocation || !nextLocation) return unavailableConnector;
const distanceKm = haversineDistanceKm(currentLocation, nextLocation); const distanceKm = haversineDistanceKm(currentLocation, nextLocation);
if (distanceKm === null) return null; if (distanceKm === null) return unavailableConnector;
const walkingMinutes = (distanceKm / WALKING_SPEED_KMH) * 60; const walkingMinutes = (distanceKm / WALKING_SPEED_KMH) * 60;
const drivingMinutes = (distanceKm / DRIVING_SPEED_KMH) * 60; const drivingMinutes = (distanceKm / DRIVING_SPEED_KMH) * 60;
@@ -737,6 +754,11 @@
return getFallbackLocationConnector(currentItem, nextItem); return getFallbackLocationConnector(currentItem, nextItem);
} }
function getI18nText(key: string, fallback: string): string {
const translated = $t(key);
return translated && translated !== key ? translated : fallback;
}
function editTransportationInline(transportation: Transportation) { function editTransportationInline(transportation: Transportation) {
handleEditTransportation({ detail: transportation } as CustomEvent<Transportation>); handleEditTransportation({ detail: transportation } as CustomEvent<Transportation>);
} }
@@ -2607,16 +2629,39 @@
{#if locationConnector} {#if locationConnector}
<div <div
class="mt-2 rounded-lg border border-dashed border-base-300 bg-base-100/70 px-3 py-2 text-xs opacity-80" class="mt-2 rounded-lg border border-base-300 bg-base-200/60 px-3 py-2 text-xs"
> >
<div class="flex items-center gap-2 flex-wrap"> {#if locationConnector.unavailable}
<span class="font-medium">{locationConnector.distanceLabel}</span> <div class="flex items-center gap-2 flex-wrap text-base-content/80">
<span class="opacity-50">•</span> <span class="inline-flex items-center gap-1 font-medium">
<span> <LocationMarker class="w-3.5 h-3.5" />
{locationConnector.mode === 'driving' ? '🚗' : '🚶'} {locationConnector.durationLabel}
{locationConnector.durationLabel} </span>
</span> <span class="text-base-content/40">•</span>
</div> <span class="inline-flex items-center gap-1 text-primary/80 font-medium underline underline-offset-2">
<LocationMarker class="w-3.5 h-3.5" />
{getI18nText('itinerary.directions', 'Directions')}
</span>
</div>
{:else}
<div class="flex items-center gap-2 flex-wrap text-base-content">
<span class="inline-flex items-center gap-1 font-medium">
{#if locationConnector.mode === 'driving'}
<Car class="w-3.5 h-3.5" />
{:else}
<Walk class="w-3.5 h-3.5" />
{/if}
{locationConnector.durationLabel}
</span>
<span class="text-base-content/50">•</span>
<span class="font-medium">{locationConnector.distanceLabel}</span>
<span class="text-base-content/50">•</span>
<span class="inline-flex items-center gap-1 text-primary font-medium underline underline-offset-2">
<LocationMarker class="w-3.5 h-3.5" />
{getI18nText('itinerary.directions', 'Directions')}
</span>
</div>
{/if}
</div> </div>
{/if} {/if}
</div> </div>