mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
feat(planner): open places and day routes in CoMaps
Add CoMaps, the offline OpenStreetMap navigator, alongside the existing map integrations. A geolocated place gains a CoMaps entry in its navigation menu, and a day's route toolbar gains a button that hands the day's bookended stops to CoMaps in that day's travel mode. CoMaps' route link carries only a start and a destination, so a two-stop day goes as a route and a longer one as named pins rather than silently losing the stops in between. Links use the https form so a device without CoMaps installed lands on comaps.at instead of a dead scheme. The hotel-bookend logic behind the Google Maps export was duplicated between desktop and mobile; both now share dayExportStops so the two links cannot disagree about which hotel legs are real. Addresses discussion #1904
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
calculateSegments,
|
||||
optimizeRoute,
|
||||
generateGoogleMapsUrl,
|
||||
generateCoMapsUrl,
|
||||
parsePluginProfile,
|
||||
withHotelBookends,
|
||||
} from './RouteCalculator'
|
||||
@@ -572,3 +573,43 @@ describe('calculateRouteWithLegs cache eviction', () => {
|
||||
expect(hits).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ── generateCoMapsUrl ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('generateCoMapsUrl', () => {
|
||||
const eiffel = { lat: 48.8584, lng: 2.2945, name: 'Eiffel Tower' }
|
||||
const louvre = { lat: 48.8606, lng: 2.3376, name: 'Louvre' }
|
||||
const notre = { lat: 48.8530, lng: 2.3499, name: 'Notre-Dame' }
|
||||
|
||||
it('FE-COMP-ROUTECALCULATOR-016: no stops, no link', () => {
|
||||
expect(generateCoMapsUrl([])).toBeNull()
|
||||
})
|
||||
|
||||
it('FE-COMP-ROUTECALCULATOR-017: two stops build a real route, mode included', () => {
|
||||
expect(generateCoMapsUrl([eiffel, louvre], 'walking')).toBe(
|
||||
'https://comaps.at/route?sll=48.8584,2.2945&saddr=Eiffel%20Tower'
|
||||
+ '&dll=48.8606,2.3376&daddr=Louvre&type=pedestrian',
|
||||
)
|
||||
})
|
||||
|
||||
it('FE-COMP-ROUTECALCULATOR-018: TREK profiles map onto CoMaps travel modes', () => {
|
||||
expect(generateCoMapsUrl([eiffel, louvre], 'driving')).toContain('type=vehicle')
|
||||
expect(generateCoMapsUrl([eiffel, louvre], 'cycling')).toContain('type=bicycle')
|
||||
// A plugin router has no CoMaps equivalent, so it falls back rather than
|
||||
// sending a mode CoMaps would reject.
|
||||
expect(generateCoMapsUrl([eiffel, louvre], 'plugin:ev/fast')).toContain('type=vehicle')
|
||||
})
|
||||
|
||||
it('FE-COMP-ROUTECALCULATOR-019: three stops go as pins, because a route link would drop the middle', () => {
|
||||
const url = generateCoMapsUrl([eiffel, louvre, notre])!
|
||||
expect(url).toBe(
|
||||
'https://comaps.at/map?v=1&ll=48.8584,2.2945&n=Eiffel%20Tower'
|
||||
+ '&ll=48.8606,2.3376&n=Louvre&ll=48.853,2.3499&n=Notre-Dame',
|
||||
)
|
||||
})
|
||||
|
||||
it('FE-COMP-ROUTECALCULATOR-020: a single stop is a pin, and a nameless one is labelled by position', () => {
|
||||
expect(generateCoMapsUrl([{ lat: 48.85, lng: 2.35 }]))
|
||||
.toBe('https://comaps.at/map?v=1&ll=48.85,2.35&n=48.85%2C2.35')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -116,6 +116,42 @@ export function generateGoogleMapsUrl(places: Waypoint[]): string | null {
|
||||
return `https://www.google.com/maps/dir/${stops}`
|
||||
}
|
||||
|
||||
/** A stop that can carry its name into a deep link that has somewhere to put one. */
|
||||
export type NamedWaypoint = Waypoint & { name?: string | null }
|
||||
|
||||
/** TREK's route profiles in CoMaps' vocabulary; a plugin profile has no equivalent and drives. */
|
||||
function coMapsRouteType(profile: RouteProfileKey): string {
|
||||
if (profile === 'walking') return 'pedestrian'
|
||||
if (profile === 'cycling') return 'bicycle'
|
||||
return 'vehicle'
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a day's stops in CoMaps for offline navigation (#1904).
|
||||
*
|
||||
* CoMaps has two links and they trade against each other. `route` builds real
|
||||
* turn-by-turn in the given travel mode but takes a start and a destination and
|
||||
* nothing between them; `map` takes any number of named pins but routes nothing.
|
||||
* So a two-stop day goes as a route — everything it has fits, mode included —
|
||||
* and a longer one goes as pins, because handing over the whole day and letting
|
||||
* CoMaps route leg by leg beats quietly dropping the middle of someone's plan.
|
||||
* A day that needs the full itinerary as one navigable track has the GPX export.
|
||||
*
|
||||
* https rather than `cm://` for the same reason as `getCoMapsUrlForPlace`.
|
||||
*/
|
||||
export function generateCoMapsUrl(places: NamedWaypoint[], profile: RouteProfileKey = 'driving'): string | null {
|
||||
const valid = places.filter((p) => p.lat != null && p.lng != null)
|
||||
if (valid.length === 0) return null
|
||||
const label = (p: NamedWaypoint) => encodeURIComponent(p.name?.trim() || `${p.lat},${p.lng}`)
|
||||
if (valid.length === 2) {
|
||||
const [from, to] = valid
|
||||
return `https://comaps.at/route?sll=${from.lat},${from.lng}&saddr=${label(from)}`
|
||||
+ `&dll=${to.lat},${to.lng}&daddr=${label(to)}&type=${coMapsRouteType(profile)}`
|
||||
}
|
||||
const pins = valid.map((p) => `ll=${p.lat},${p.lng}&n=${label(p)}`).join('&')
|
||||
return `https://comaps.at/map?v=1&${pins}`
|
||||
}
|
||||
|
||||
// Squared planar distance — enough for nearest-neighbor comparisons and cheaper than a full haversine.
|
||||
function sqDist(a: Waypoint, b: Waypoint): number {
|
||||
return (a.lat - b.lat) ** 2 + (a.lng - b.lng) ** 2
|
||||
|
||||
Reference in New Issue
Block a user