fix: keep itinerary connectors visible when route data is unavailable
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user