SkateSpots/src/lib/directions.ts
SkateSpots fdf33d2c45 feat(directions): public-transport mode (Transitous) + open-in-maps handoff
- routing.ts: add a 'transit' travel mode via Transitous (MOTIS 2 API, keyless,
  open CORS), with a local precision-7 polyline decoder and per-leg parsing.
  Road modes stay on keyless OSRM; moto remains ≈car and honestly labelled.
- RouteDialog: transit draws each leg (walk dashed grey, transit coloured by
  mode) and summarises depart→arrive, duration, transfers and line chips, with
  Transitous/OpenStreetMap attribution.
- directions.ts: opt-in 'Open in' handoff to Google / Apple / OpenStreetMap
  (+ Organic Maps via geo: on Android), mode-aware. In-app routing stays the
  default; the description notes transit shares the trip with Transitous.
- No scooter/moped profile (deliberate).
2026-09-27 10:52:26 +07:00

101 lines
3.6 KiB
TypeScript

/**
* "Open in maps" handoff — deep links to external navigation apps.
* ─────────────────────────────────────────────────────────────────────────────
* The in-app RouteDialog deliberately keeps routing on our own map (OSRM +
* Transitous) so nothing goes to Google by default. This module is the *opt-in*
* escape hatch: build links so a user who wants real turn-by-turn / offline
* navigation can hand the trip to another app.
*
* Providers are chosen for reliable, keyless web URLs that also deep-link into
* their native apps:
* • Google Maps — supports driving / walking / bicycling / transit
* • Apple Maps — iOS/macOS (opens the web version elsewhere)
* • OpenStreetMap — car / bike / foot (no transit)
* • Organic Maps — Android `geo:` intent (privacy-first, offline)
*
* Handing off *does* share the trip with that provider — hence opt-in.
*/
import { Capacitor } from '@capacitor/core';
import type { TravelMode } from '@/lib/routing';
export interface DirectionsLink {
id: string;
label: string;
url: string;
}
interface Point {
lat: number;
lng: number;
}
const GOOGLE_MODE: Record<TravelMode, string> = {
foot: 'walking',
bike: 'bicycling',
moto: 'driving',
car: 'driving',
transit: 'transit',
};
function googleUrl(from: Point, to: Point, mode: TravelMode): string {
return `https://www.google.com/maps/dir/?api=1&origin=${from.lat},${from.lng}&destination=${to.lat},${to.lng}&travelmode=${GOOGLE_MODE[mode]}`;
}
/** Apple Maps "dirflg": w = walking, r = public transit, d = driving. */
function appleFlag(mode: TravelMode): string {
if (mode === 'foot') return 'w';
if (mode === 'transit') return 'r';
return 'd'; // car / moto / bike
}
function appleUrl(from: Point, to: Point, mode: TravelMode): string {
return `https://maps.apple.com/?saddr=${from.lat},${from.lng}&daddr=${to.lat},${to.lng}&dirflg=${appleFlag(mode)}`;
}
/** OSM has no public-transport routing; returns null for transit. */
function osmEngine(mode: TravelMode): string | null {
if (mode === 'foot') return 'fossgis_osrm_foot';
if (mode === 'bike') return 'fossgis_osrm_bike';
if (mode === 'transit') return null;
return 'fossgis_osrm_car'; // car / moto
}
function osmUrl(from: Point, to: Point, mode: TravelMode): string | null {
const engine = osmEngine(mode);
if (!engine) return null;
return `https://www.openstreetmap.org/directions?engine=${engine}&route=${from.lat},${from.lng};${to.lat},${to.lng}`;
}
/** `geo:` intent — Android hands it to Organic Maps (or the default map app). */
function organicUrl(to: Point, label: string): string {
return `geo:${to.lat},${to.lng}?q=${to.lat},${to.lng}(${encodeURIComponent(label)})`;
}
export function buildDirectionsLinks(
from: Point,
to: Point,
mode: TravelMode,
destinationLabel: string,
): DirectionsLink[] {
const links: DirectionsLink[] = [
{ id: 'google', label: 'Google Maps', url: googleUrl(from, to, mode) },
{ id: 'apple', label: 'Apple Maps', url: appleUrl(from, to, mode) },
];
const osm = osmUrl(from, to, mode);
if (osm) links.push({ id: 'osm', label: 'OpenStreetMap', url: osm });
if (Capacitor.getPlatform() === 'android') {
links.push({ id: 'organic', label: 'Organic Maps', url: organicUrl(to, destinationLabel) });
}
return links;
}
/**
* Open an external URL. Capacitor intercepts the `_system` target and hands the
* URL to the OS (system browser / registered app); on the web it's a new tab.
*/
export function openExternal(url: string): void {
window.open(url, '_system');
}