414 lines
11 KiB
TypeScript
414 lines
11 KiB
TypeScript
import { location } from "$lib/components/lnv/location.svelte";
|
|
import { map } from "$lib/components/lnv/map.svelte";
|
|
import say, { findLocaleForValhallaLanguage } from "./TTS";
|
|
import type { ValhallaRequest } from "./ValhallaRequest";
|
|
import type { LngLatBoundsLike } from "maplibre-gl";
|
|
import { generateVoiceGuidance } from "./VoiceGuidance";
|
|
import { keepScreenOn } from "tauri-plugin-keep-screen-on-api";
|
|
import { m } from "$lang/messages";
|
|
|
|
export const routing = $state({
|
|
geojson: {
|
|
route: null as GeoJSON.Feature | null,
|
|
routePast: null as GeoJSON.Feature | null,
|
|
al0: null as GeoJSON.Feature | null,
|
|
al1: null as GeoJSON.Feature | null,
|
|
},
|
|
currentTrip: null as Trip | null,
|
|
currentTripInfo: {
|
|
maneuverIdx: 0,
|
|
past: [] as WorldLocation[],
|
|
route: [] as WorldLocation[],
|
|
int: null as NodeJS.Timeout | null,
|
|
isOffRoute: false,
|
|
currentManeuver: null as Maneuver | null,
|
|
},
|
|
});
|
|
|
|
export function resetRouting() {
|
|
routing.geojson.route = null;
|
|
routing.geojson.routePast = null;
|
|
routing.geojson.al0 = null;
|
|
routing.geojson.al1 = null;
|
|
}
|
|
|
|
export async function fetchRoute(server: string, request: ValhallaRequest) {
|
|
try {
|
|
const res = await fetch(
|
|
server + "/route?json=" + JSON.stringify(request),
|
|
).then((res) => res.json());
|
|
|
|
console.log(res);
|
|
return res;
|
|
} catch (error) {
|
|
console.error("Error calculating route:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function tripToGeoJSON(trip: Trip): GeoJSON.Feature {
|
|
const polyline = decodePolyline(trip.legs[0].shape);
|
|
return {
|
|
type: "Feature",
|
|
properties: {},
|
|
geometry: {
|
|
type: "LineString",
|
|
coordinates: polyline.map((p) => [p.lon, p.lat]),
|
|
},
|
|
};
|
|
}
|
|
|
|
function geometryToGeoJSON(polyline: WorldLocation[]): GeoJSON.Feature {
|
|
return {
|
|
type: "Feature",
|
|
properties: {},
|
|
geometry: {
|
|
type: "LineString",
|
|
coordinates: polyline.map((p) => [p.lon, p.lat]),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function decodePolyline(encoded: string): WorldLocation[] {
|
|
const points = [];
|
|
let index = 0;
|
|
const len = encoded.length;
|
|
let lat = 0;
|
|
let lng = 0;
|
|
|
|
while (index < len) {
|
|
let shift = 0;
|
|
let result = 0;
|
|
let byte;
|
|
do {
|
|
byte = encoded.charCodeAt(index++) - 63;
|
|
result |= (byte & 0x1f) << shift;
|
|
shift += 5;
|
|
} while (byte >= 0x20);
|
|
|
|
const deltaLat = result & 1 ? ~(result >> 1) : result >> 1;
|
|
lat += deltaLat;
|
|
|
|
shift = 0;
|
|
result = 0;
|
|
do {
|
|
byte = encoded.charCodeAt(index++) - 63;
|
|
result |= (byte & 0x1f) << shift;
|
|
shift += 5;
|
|
} while (byte >= 0x20);
|
|
|
|
const deltaLng = result & 1 ? ~(result >> 1) : result >> 1;
|
|
lng += deltaLng;
|
|
|
|
// Convert the latitude and longitude to decimal format with six digits of precision
|
|
points.push({
|
|
lat: lat / 1000000, // Divide by 1,000,000 for six digits of precision
|
|
lon: lng / 1000000, // Divide by 1,000,000 for six digits of precision
|
|
});
|
|
}
|
|
|
|
return points;
|
|
}
|
|
|
|
export function drawAllRoutes(trips: Trip[]) {
|
|
routing.geojson.routePast = null;
|
|
routing.geojson.route = tripToGeoJSON(trips[0]);
|
|
if (trips[1]) routing.geojson.al0 = tripToGeoJSON(trips[1]);
|
|
if (trips[2]) routing.geojson.al1 = tripToGeoJSON(trips[2]);
|
|
}
|
|
|
|
export function drawRoute(trip: Trip) {
|
|
routing.geojson.route = tripToGeoJSON(trip);
|
|
}
|
|
|
|
function drawCurrentTrip() {
|
|
if (!routing.currentTrip) return;
|
|
routing.geojson.route = geometryToGeoJSON(routing.currentTripInfo.route);
|
|
routing.geojson.routePast = geometryToGeoJSON(
|
|
routing.currentTripInfo.past.flat(),
|
|
);
|
|
}
|
|
|
|
export async function startRoute(trip: Trip) {
|
|
if (window.__TAURI__) {
|
|
await keepScreenOn(true);
|
|
}
|
|
routing.currentTrip = trip;
|
|
removeAllRoutes();
|
|
routing.geojson.route = tripToGeoJSON(trip);
|
|
routing.currentTripInfo.maneuverIdx = 0;
|
|
routing.currentTripInfo.past = [];
|
|
routing.currentTripInfo.isOffRoute = false;
|
|
|
|
drawRoute(trip);
|
|
routing.currentTripInfo.currentManeuver =
|
|
routing.currentTrip.legs[0].maneuvers[0];
|
|
|
|
routing.currentTripInfo.int = setInterval(tickRoute, 500);
|
|
}
|
|
|
|
let hasAnnouncedPreInstruction = false;
|
|
const USE_LANDMARK_INSTRUCTIONS = false;
|
|
|
|
async function tickRoute() {
|
|
const trip = routing.currentTrip;
|
|
const info = routing.currentTripInfo;
|
|
if (!trip) return;
|
|
|
|
const currentManeuver =
|
|
trip.legs[0].maneuvers[routing.currentTripInfo.maneuverIdx];
|
|
if (!currentManeuver) {
|
|
// No more maneuvers, stop navigation
|
|
stopNavigation();
|
|
return;
|
|
}
|
|
|
|
const bgi = currentManeuver.begin_shape_index;
|
|
const loc = {
|
|
lat: location.lat,
|
|
lon: location.lng,
|
|
};
|
|
const polyline = decodePolyline(trip.legs[0].shape);
|
|
|
|
// Check if the user location is on the last point of the entire route
|
|
if (isOnPoint(loc, polyline[polyline.length - 1])) {
|
|
console.log("Reached destination!");
|
|
stopNavigation();
|
|
return;
|
|
}
|
|
|
|
// Check if the user is on the route
|
|
if (!isOnShape(loc, polyline, 30)) {
|
|
console.log("Off route!");
|
|
if (!info.isOffRoute) {
|
|
say(m["routing.off-route"]());
|
|
}
|
|
info.isOffRoute = true;
|
|
// TODO: Implement re-routing logic
|
|
return;
|
|
} else {
|
|
if (info.isOffRoute) {
|
|
say(m["routing.back-on-route"]());
|
|
}
|
|
info.isOffRoute = false;
|
|
}
|
|
|
|
if (
|
|
currentManeuver.verbal_pre_transition_instruction &&
|
|
!hasAnnouncedPreInstruction
|
|
) {
|
|
const distanceToEnd = calculateDistance(loc, polyline[bgi]);
|
|
// console.log("Distance to end of current maneuver: ", distanceToEnd, " meters");
|
|
// console.log("Speed: ", location.speed, " km/h");
|
|
const verbalDistance = verbalPreInstructionDistance(
|
|
location.speed || 50, // Assuming location has a speed property
|
|
);
|
|
if (distanceToEnd <= verbalDistance) {
|
|
hasAnnouncedPreInstruction = true;
|
|
const instruction = USE_LANDMARK_INSTRUCTIONS
|
|
? await generateVoiceGuidance(currentManeuver, polyline)
|
|
: currentManeuver.verbal_pre_transition_instruction;
|
|
console.log(
|
|
"[Verbal instruction] ",
|
|
// currentManeuver.verbal_pre_transition_instruction,
|
|
instruction,
|
|
);
|
|
const locale = findLocaleForValhallaLanguage(trip.language);
|
|
say(instruction, locale);
|
|
}
|
|
}
|
|
|
|
// Check if the user is past the current maneuver
|
|
// Checks if the user is still on the current maneuver's polyline
|
|
if (!isOnShape(loc, polyline.slice(bgi))) {
|
|
return; // User is not on the current maneuver's polyline, do not update
|
|
}
|
|
|
|
// Update the past and current route polylines
|
|
info.past = polyline.slice(0, bgi + 1);
|
|
info.route = polyline.slice(bgi);
|
|
|
|
// Update the geojson
|
|
drawCurrentTrip();
|
|
|
|
// announce the "verbal_post_transition_instruction"
|
|
if (currentManeuver.verbal_post_transition_instruction) {
|
|
hasAnnouncedPreInstruction = false;
|
|
const distanceToEnd = calculateDistance(
|
|
loc,
|
|
polyline[
|
|
trip.legs[0].maneuvers[routing.currentTripInfo.maneuverIdx + 1]
|
|
.begin_shape_index
|
|
],
|
|
);
|
|
if (distanceToEnd >= 200) {
|
|
console.log(
|
|
"[Verbal instruction] ",
|
|
currentManeuver.verbal_post_transition_instruction,
|
|
);
|
|
const locale = findLocaleForValhallaLanguage(trip.language);
|
|
say(currentManeuver.verbal_post_transition_instruction, locale);
|
|
}
|
|
}
|
|
|
|
// Advance to the next maneuver
|
|
info.maneuverIdx++;
|
|
if (info.maneuverIdx >= trip.legs[0].maneuvers.length) {
|
|
// No more maneuvers
|
|
stopNavigation();
|
|
return;
|
|
}
|
|
|
|
info.currentManeuver = trip.legs[0].maneuvers[info.maneuverIdx];
|
|
|
|
// queueSpeech(info.currentManeuver.verbal_pre_transition_instruction || "");
|
|
// queueSpeech(info.currentManeuver.verbal_post_transition_instruction || "");
|
|
|
|
// TODO: verbal instructions
|
|
}
|
|
|
|
function verbalPreInstructionDistance(speed: number): number {
|
|
return (Math.min(speed, 30) * 2.222 + 37.144) * 2;
|
|
}
|
|
|
|
export function stopNavigation() {
|
|
if (routing.currentTripInfo.int) {
|
|
clearInterval(routing.currentTripInfo.int);
|
|
routing.currentTripInfo.int = null;
|
|
}
|
|
routing.currentTrip = null;
|
|
map.updateMapPadding(); // TODO: REMOVE
|
|
removeAllRoutes();
|
|
if (window.__TAURI__) {
|
|
keepScreenOn(false);
|
|
}
|
|
}
|
|
|
|
// function getUserLocation(): WorldLocation {
|
|
// // return geolocate.currentLocation!;
|
|
// return {
|
|
// lat: location.lat,
|
|
// lon: location.lng,
|
|
// };
|
|
// // const lnglat = window.geolocate._userLocationDotMarker.getLngLat();
|
|
// // return { lat: lnglat.lat, lon: lnglat.lng };
|
|
// // console.log(map.value!)
|
|
// // return {
|
|
// // lat: 0,
|
|
// // lon: 0
|
|
// // }
|
|
// }
|
|
|
|
function isOnLine(
|
|
location: WorldLocation,
|
|
from: WorldLocation,
|
|
to: WorldLocation,
|
|
toleranceMeters = 12,
|
|
) {
|
|
// Convert the tolerance to degrees (approximation)
|
|
const tolerance = toleranceMeters / 111320; // 1 degree latitude ≈ 111.32 km
|
|
|
|
// Calculate the vector components
|
|
const dx = to.lon - from.lon;
|
|
const dy = to.lat - from.lat;
|
|
|
|
// Calculate the projection of the location onto the line segment
|
|
const t =
|
|
((location.lon - from.lon) * dx + (location.lat - from.lat) * dy) /
|
|
(dx * dx + dy * dy);
|
|
|
|
// Clamp t to the range [0, 1] to ensure the projection is on the segment
|
|
const clampedT = Math.max(0, Math.min(1, t));
|
|
|
|
// Calculate the closest point on the line segment
|
|
const closestPoint = {
|
|
lon: from.lon + clampedT * dx,
|
|
lat: from.lat + clampedT * dy,
|
|
};
|
|
|
|
// Calculate the distance from the location to the closest point
|
|
const distance = Math.sqrt(
|
|
Math.pow(location.lon - closestPoint.lon, 2) +
|
|
Math.pow(location.lat - closestPoint.lat, 2),
|
|
);
|
|
|
|
// Check if the distance is within the tolerance
|
|
return distance <= tolerance;
|
|
}
|
|
|
|
function isOnPoint(location: WorldLocation, point: WorldLocation) {
|
|
// Convert the 6-meter tolerance to degrees (approximation)
|
|
const tolerance = 6 / 111320; // 1 degree latitude ≈ 111.32 km
|
|
// Calculate the distance from the location to the point
|
|
const distance = Math.sqrt(
|
|
Math.pow(location.lon - point.lon, 2) +
|
|
Math.pow(location.lat - point.lat, 2),
|
|
);
|
|
// Check if the distance is within the tolerance
|
|
return distance <= tolerance;
|
|
}
|
|
|
|
function isOnShape(
|
|
location: WorldLocation,
|
|
shape: WorldLocation[],
|
|
toleranceMeters = 12,
|
|
) {
|
|
for (let i = 0; i < shape.length - 1; i++) {
|
|
if (isOnLine(location, shape[i], shape[i + 1], toleranceMeters)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function calculateDistance(
|
|
point1: WorldLocation,
|
|
point2: WorldLocation,
|
|
): number {
|
|
const R = 6371000; // Earth's radius in meters
|
|
const lat1 = point1.lat * (Math.PI / 180);
|
|
const lat2 = point2.lat * (Math.PI / 180);
|
|
const deltaLat = (point2.lat - point1.lat) * (Math.PI / 180);
|
|
const deltaLon = (point2.lon - point1.lon) * (Math.PI / 180);
|
|
|
|
const a =
|
|
Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
|
|
Math.cos(lat1) *
|
|
Math.cos(lat2) *
|
|
Math.sin(deltaLon / 2) *
|
|
Math.sin(deltaLon / 2);
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
|
return R * c; // Distance in meters
|
|
}
|
|
|
|
export function zoomToPoints(
|
|
from: WorldLocation,
|
|
to: WorldLocation,
|
|
map: maplibregl.Map,
|
|
) {
|
|
const getBoundingBox = (
|
|
point1: [number, number],
|
|
point2: [number, number],
|
|
): LngLatBoundsLike => {
|
|
const [lng1, lat1] = point1;
|
|
const [lng2, lat2] = point2;
|
|
|
|
const sw = [Math.min(lng1, lng2), Math.min(lat1, lat2)] as [number, number];
|
|
const ne = [Math.max(lng1, lng2), Math.max(lat1, lat2)] as [number, number];
|
|
|
|
return [sw, ne];
|
|
};
|
|
|
|
map.fitBounds(getBoundingBox([from.lon, from.lat], [to.lon, to.lat]), {
|
|
padding: 40,
|
|
});
|
|
}
|
|
|
|
export function removeAllRoutes() {
|
|
routing.geojson.route = null;
|
|
routing.geojson.routePast = null;
|
|
routing.geojson.al0 = null;
|
|
routing.geojson.al1 = null;
|
|
}
|