diff --git a/client/src/components/Map/RouteCalculator.test.ts b/client/src/components/Map/RouteCalculator.test.ts index 12464f562..a4a11d174 100644 --- a/client/src/components/Map/RouteCalculator.test.ts +++ b/client/src/components/Map/RouteCalculator.test.ts @@ -256,6 +256,19 @@ describe('generateGoogleMapsUrl', () => { expect(result).toContain('48.85,2.35') expect(result).toContain('48.86,2.36') }) + + it('FE-COMP-ROUTECALCULATOR-015b: a walk or ride carries its mode, a drive keeps the path link', () => { + const stops = [{ lat: 48.85, lng: 2.35 }, { lat: 48.86, lng: 2.36 }, { lat: 48.87, lng: 2.37 }] + expect(generateGoogleMapsUrl(stops, 'walking')).toBe( + 'https://www.google.com/maps/dir/?api=1&origin=48.85,2.35&destination=48.87,2.37&waypoints=48.86%2C2.36&travelmode=walking', + ) + expect(generateGoogleMapsUrl(stops, 'cycling')).toContain('&travelmode=bicycling') + expect(generateGoogleMapsUrl(stops, 'driving')).toBe('https://www.google.com/maps/dir/48.85,2.35/48.86,2.36/48.87,2.37') + }) + + it('FE-COMP-ROUTECALCULATOR-015c: a stop on the equator or the prime meridian is kept', () => { + expect(generateGoogleMapsUrl([{ lat: 0, lng: 0 }, { lat: 0, lng: 1 }])).toBe('https://www.google.com/maps/dir/0,0/0,1') + }) }) // ── withHotelBookends (#1275: draw the hotel → first / last → hotel legs) ──────── diff --git a/client/src/components/Map/RouteCalculator.ts b/client/src/components/Map/RouteCalculator.ts index 2df83f3ee..fe36e400d 100644 --- a/client/src/components/Map/RouteCalculator.ts +++ b/client/src/components/Map/RouteCalculator.ts @@ -382,14 +382,28 @@ export function withHotelBookends( return out } -export function generateGoogleMapsUrl(places: Waypoint[]): string | null { - const valid = places.filter((p) => p.lat && p.lng) +/** Google's directions modes for the profiles it has; anything else leaves the mode to Google. */ +const GOOGLE_TRAVEL_MODE: Partial> = { walking: 'walking', cycling: 'bicycling' } + +/** + * Google Maps for a list of stops. A walking or cycling route goes as the + * documented directions link, which carries the mode; a drive keeps the short + * path link it always had. A stop on the equator or the prime meridian counts. + */ +export function generateGoogleMapsUrl(places: Waypoint[], profile?: RouteProfileKey): string | null { + const valid = places.filter((p) => p.lat != null && p.lng != null) if (valid.length === 0) return null if (valid.length === 1) { return `https://www.google.com/maps/search/?api=1&query=${valid[0].lat},${valid[0].lng}` } - const stops = valid.map((p) => `${p.lat},${p.lng}`).join('/') - return `https://www.google.com/maps/dir/${stops}` + const mode = profile && GOOGLE_TRAVEL_MODE[profile] + const point = (p: Waypoint) => `${p.lat},${p.lng}` + if (mode) { + const between = valid.slice(1, -1).map(point).join('|') + return `https://www.google.com/maps/dir/?api=1&origin=${point(valid[0])}&destination=${point(valid[valid.length - 1])}` + + (between ? `&waypoints=${encodeURIComponent(between)}` : '') + `&travelmode=${mode}` + } + return `https://www.google.com/maps/dir/${valid.map(point).join('/')}` } /** A stop that can carry its name into a deep link that has somewhere to put one. */ diff --git a/client/src/components/Planner/dayRouteApps.ts b/client/src/components/Planner/dayRouteApps.ts index 9501eaf4d..50cea98af 100644 --- a/client/src/components/Planner/dayRouteApps.ts +++ b/client/src/components/Planner/dayRouteApps.ts @@ -72,7 +72,7 @@ export function dayRouteTargets(stops: NamedWaypoint[], profile: RouteProfileKey const push = (target: Omit, url: string | null) => { if (url) out.push({ ...target, url }); }; - push({ id: 'google', label: 'Google Maps' }, generateGoogleMapsUrl(stops)); + push({ id: 'google', label: 'Google Maps' }, generateGoogleMapsUrl(stops, profile)); if (showsAppleMaps()) push({ id: 'apple', label: 'Apple Maps' }, generateAppleMapsUrl(stops, profile)); push({ id: 'comaps', label: 'CoMaps' }, generateCoMapsUrl(stops, profile)); const located = stops.filter((p) => p.lat != null && p.lng != null).length; diff --git a/client/src/components/Tours/tourRouteApps.test.ts b/client/src/components/Tours/tourRouteApps.test.ts index f05ae04a6..cecf02bc7 100644 --- a/client/src/components/Tours/tourRouteApps.test.ts +++ b/client/src/components/Tours/tourRouteApps.test.ts @@ -39,6 +39,16 @@ describe('tourRouteApps (#2812)', () => { const targets = tourRouteTargets('bike', line(2), 'Ride'); expect(targets.map((target) => target.id)).toEqual(['google', 'comaps']); expect(targets.find((target) => target.id === 'comaps')?.url).toContain('type=bicycle'); - expect(targets.find((target) => target.id === 'google')?.url).toBe('https://www.google.com/maps/dir/0,0/0,0.01'); + expect(targets.find((target) => target.id === 'google')?.url).toBe( + 'https://www.google.com/maps/dir/?api=1&origin=0,0&destination=0,0.01&travelmode=bicycling' + ); + }); + + it('FE-TOUR-ROUTEAPPS-005: a longer tour goes to Google on foot with its stops between, and not to CoMaps', () => { + const targets = tourRouteTargets('hike', line(5), 'Ridge'); + expect(targets.map((target) => target.id)).toEqual(['google']); + const url = targets[0].url; + expect(url).toContain('&travelmode=walking'); + expect(url).toContain('&waypoints=0%2C0.01%7C0%2C0.02%7C0%2C0.03'); }); }); diff --git a/client/src/components/Tours/tourRouteApps.ts b/client/src/components/Tours/tourRouteApps.ts index 0c94b48d6..2162839b5 100644 --- a/client/src/components/Tours/tourRouteApps.ts +++ b/client/src/components/Tours/tourRouteApps.ts @@ -67,8 +67,9 @@ function spreadIndices(line: ReadonlyArray, max: numb /** * The map apps a tour's route can be opened in, with the tour's mode. Only the - * apps that take the whole route: one that only gets the start (Waze, a geo: - * link) is what the tour's Navigation button already offers. + * apps that route the whole way: one that only gets the start (Waze, a geo: + * link) is what the tour's Navigation button already offers, and CoMaps routes + * a start and an end only, so with stops between it would get unrouted pins. */ export function tourRouteTargets( type: TourTypeKey, @@ -77,5 +78,8 @@ export function tourRouteTargets( ): DayRouteTarget[] { const profile = tourAppProfile(type); if (!profile) return []; - return dayRouteTargets(tourRouteStops(line, name), profile).filter((target) => !target.firstStopOnly); + const stops = tourRouteStops(line, name); + return dayRouteTargets(stops, profile).filter( + (target) => !target.firstStopOnly && !(target.id === 'comaps' && stops.length > 2) + ); }