From f4d7221efd1a94cf9d7e4aa31b5af31da39ea007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20R=C4=85czy?= Date: Sun, 23 Jul 2023 17:50:25 -0700 Subject: [PATCH 1/7] Initial implementation with ACTION_SET_NAVIGATION_DESTINATION action and navigationDestination prop --- src/actions/history.js | 9 +++++-- src/actions/index.js | 20 ++++++++++++++ src/actions/types.js | 3 +++ src/components/Dashboard/index.jsx | 5 ++-- src/components/Navigation/index.jsx | 42 ++++++++++++++++++++++++++++- src/reducers/globalState.js | 3 +++ src/url.js | 17 ++++++++++++ 7 files changed, 94 insertions(+), 5 deletions(-) diff --git a/src/actions/history.js b/src/actions/history.js index 590b2f291..859dcf439 100644 --- a/src/actions/history.js +++ b/src/actions/history.js @@ -1,6 +1,6 @@ import { LOCATION_CHANGE } from 'connected-react-router'; -import { getDongleID, getZoom, getPrimeNav, getClipsNav } from '../url'; -import { primeNav, selectDevice, selectRange } from './index'; +import { getDongleID, getZoom, getPrimeNav, getClipsNav, getNavigationNav } from '../url'; +import { primeNav, selectDevice, selectRange, navigateToDestination } from './index'; import { clipsExit, fetchClipsDetails, fetchClipsList } from './clips'; export const onHistoryMiddleware = ({ dispatch, getState }) => (next) => (action) => { @@ -24,6 +24,11 @@ export const onHistoryMiddleware = ({ dispatch, getState }) => (next) => (action dispatch(primeNav(pathPrimeNav)); } + const pathNavigationNav = getNavigationNav(action.payload.location.pathname, action.payload.location.search); + if (pathNavigationNav && pathNavigationNav != state.navigationNav) { + dispatch(navigateToDestination(pathNavigationNav.lat, pathNavigationNav.long)); + } + const pathClipsNav = getClipsNav(action.payload.location.pathname); if (pathClipsNav === null && state.clips) { dispatch(clipsExit()); diff --git a/src/actions/index.js b/src/actions/index.js index 22bc8640b..5fc3aa69f 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -132,6 +132,26 @@ export function primeNav(nav, allowPathChange = true) { }; } +export function navigateToDestination(lat, long) { + return (dispatch, getState) => { + const state = getState(); + if (!state.dongleId) { + return; + } + + let destination = { + latitude: lat, + longitude: long, + }; + if (state.navigationDestination !== destination) { + dispatch({ + type: Types.ACTION_SET_NAVIGATION_DESTINATION, + navigationDestination: destination, + }); + } + }; +} + export function fetchSharedDevice(dongleId) { return async (dispatch) => { try { diff --git a/src/actions/types.js b/src/actions/types.js index 86920c155..c3ee0e32a 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -18,6 +18,9 @@ export const ACTION_PRIME_NAV = 'ACTION_PRIME_NAV'; export const ACTION_PRIME_SUBSCRIPTION = 'ACTION_PRIME_SUBSCRIPTION'; export const ACTION_PRIME_SUBSCRIBE_INFO = 'ACTION_PRIME_SUBSCRIBE_INFO'; +// map +export const ACTION_SET_NAVIGATION_DESTINATION = 'ACTION_SET_NAVIGATION_DESTINATION'; + // playback export const ACTION_SEEK = 'action_seek'; export const ACTION_PAUSE = 'action_pause'; diff --git a/src/components/Dashboard/index.jsx b/src/components/Dashboard/index.jsx index a811c5f85..4530205c3 100644 --- a/src/components/Dashboard/index.jsx +++ b/src/components/Dashboard/index.jsx @@ -18,7 +18,7 @@ const DashboardLoading = () => ( ); -const Dashboard = ({ primeNav, device, dongleId }) => { +const Dashboard = ({ primeNav, device, dongleId, navigationDestination }) => { if (!device || !dongleId) { return null; } @@ -30,7 +30,7 @@ const Dashboard = ({ primeNav, device, dongleId }) => { ? : ( <> - + @@ -44,6 +44,7 @@ const stateToProps = Obstruction({ dongleId: 'dongleId', primeNav: 'primeNav', device: 'device', + navigationDestination: 'navigationDestination', }); export default connect(stateToProps)(Dashboard); diff --git a/src/components/Navigation/index.jsx b/src/components/Navigation/index.jsx index 423fb56ec..1f191672d 100644 --- a/src/components/Navigation/index.jsx +++ b/src/components/Navigation/index.jsx @@ -350,7 +350,7 @@ class Navigation extends Component { } componentDidUpdate(prevProps, prevState) { - const { dongleId, device } = this.props; + const { dongleId, device, navigationDestination } = this.props; const { geoLocateCoords, search, carLastLocation, carNetworkLocation, searchSelect, favoriteLocations } = this.state; if ((carLastLocation && !prevState.carLastLocation) || (carNetworkLocation && !prevState.carNetworkLocation) @@ -359,6 +359,12 @@ class Navigation extends Component { this.flyToMarkers(); } + if (prevProps.navigationDestination !== navigationDestination) { + this.getDeviceLastLocation().then(() => { + this.setDestination(navigationDestination.latitude, navigationDestination.longitude); + }) + } + if (prevProps.dongleId !== dongleId) { this.setState({ ...initialState, @@ -399,6 +405,40 @@ class Navigation extends Component { } } + setDestination(latitude, longitude) { + this.focus(); + + const item = { + favoriteId: null, + favoriteIcon: null, + address: { + label: '', + }, + title: '', + resultType: 'place', + position: { + lat: latitude, + lng: longitude, + }, + }; + this.onSearchSelect(item, 'pin'); + reverseLookup([longitude, latitude], false).then((location) => { + if (!location) { + return; + } + + this.setState((prevState) => ({ + searchSelect: { + ...prevState.searchSelect, + address: { + label: location.details, + }, + title: location.place, + }, + })); + }); + } + updateDevice() { if (Demo.isDemo()) { return; diff --git a/src/reducers/globalState.js b/src/reducers/globalState.js index f22abb66d..0d9108ff2 100644 --- a/src/reducers/globalState.js +++ b/src/reducers/globalState.js @@ -431,6 +431,9 @@ export default function reducer(_state, action) { end: action.end, }; break; + case Types.ACTION_SET_NAVIGATION_DESTINATION: + state.navigationDestination = action.navigationDestination + break; default: return state; } diff --git a/src/url.js b/src/url.js index c8dfea558..cdbddd6fe 100644 --- a/src/url.js +++ b/src/url.js @@ -48,3 +48,20 @@ export function getClipsNav(pathname) { } return null; } + +export function getNavigationNav(pathname, search) { + let parts = pathname.split('/'); + parts = parts.filter((m) => m.length); + + if (parts.length >= 2 && parts[0] !== 'auth' && parts[1] == 'navigate') { + let params = new URLSearchParams(search); + if (params.has('lat') && params.has('long')) { + return { + lat: parseFloat(params.get('lat')), + long: parseFloat(params.get('long')), + }; + } + return {}; + } + return null; +} \ No newline at end of file From 533054619ac6251afcbc6e81878fbfa40619b510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20R=C4=85czy?= Date: Wed, 26 Jul 2023 11:14:56 -0700 Subject: [PATCH 2/7] Add support for maps urls --- src/actions/history.js | 2 +- src/actions/index.js | 37 ++++++++++++++++++++++++++----------- src/url.js | 7 ++++++- src/utils/maps.js | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 src/utils/maps.js diff --git a/src/actions/history.js b/src/actions/history.js index 859dcf439..5fb9f3046 100644 --- a/src/actions/history.js +++ b/src/actions/history.js @@ -26,7 +26,7 @@ export const onHistoryMiddleware = ({ dispatch, getState }) => (next) => (action const pathNavigationNav = getNavigationNav(action.payload.location.pathname, action.payload.location.search); if (pathNavigationNav && pathNavigationNav != state.navigationNav) { - dispatch(navigateToDestination(pathNavigationNav.lat, pathNavigationNav.long)); + dispatch(navigateToDestination(pathNavigationNav)); } const pathClipsNav = getClipsNav(action.payload.location.pathname); diff --git a/src/actions/index.js b/src/actions/index.js index 5fc3aa69f..e16bc7243 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -9,6 +9,7 @@ import { resetPlayback, selectLoop } from '../timeline/playback'; import { getSegmentFetchRange, hasRoutesData } from '../timeline/segments'; import { getClipsNav } from '../url'; import { getDeviceFromState, deviceVersionAtLeast } from '../utils'; +import { coordinatesFromMapsUrl } from '../utils/maps'; let routesRequest = null; @@ -132,23 +133,37 @@ export function primeNav(nav, allowPathChange = true) { }; } -export function navigateToDestination(lat, long) { +export function navigateToDestination(destination) { return (dispatch, getState) => { const state = getState(); if (!state.dongleId) { return; } - - let destination = { - latitude: lat, - longitude: long, - }; - if (state.navigationDestination !== destination) { - dispatch({ - type: Types.ACTION_SET_NAVIGATION_DESTINATION, - navigationDestination: destination, - }); + + console.log(destination); + let {url, lat, long} = destination; + var latLongPromise = null; + if (url) { + latLongPromise = coordinatesFromMapsUrl(url); + } else if (lat && long) { + latLongPromise = Promise.resolve([lat, long]); + } else { + return; } + + latLongPromise.then((coords) => { + let destination = { + latitude: coords[0], + longitude: coords[1], + }; + + if (state.navigationDestination !== destination) { + dispatch({ + type: Types.ACTION_SET_NAVIGATION_DESTINATION, + navigationDestination: destination, + }); + } + }); }; } diff --git a/src/url.js b/src/url.js index cdbddd6fe..75dd5546b 100644 --- a/src/url.js +++ b/src/url.js @@ -55,7 +55,12 @@ export function getNavigationNav(pathname, search) { if (parts.length >= 2 && parts[0] !== 'auth' && parts[1] == 'navigate') { let params = new URLSearchParams(search); - if (params.has('lat') && params.has('long')) { + if (params.has('url')) { + return { + url: params.get('url'), + } + } + else if (params.has('lat') && params.has('long')) { return { lat: parseFloat(params.get('lat')), long: parseFloat(params.get('long')), diff --git a/src/utils/maps.js b/src/utils/maps.js new file mode 100644 index 000000000..94a9ca002 --- /dev/null +++ b/src/utils/maps.js @@ -0,0 +1,37 @@ +import { forwardLookup } from "./geocode"; + +export async function coordinatesFromMapsUrl(urlString) { + let url = new URL(urlString); + if (url.hostname === 'maps.apple.com') { + if (!url.searchParams.has('ll')) { + return null; + } + + let ll = url.searchParams.get('ll'); + let coords = ll.split(',').map((x) => parseFloat(x)); + return coords; + } else if (['maps.google.com', 'google.com', 'maps.app.goo.gl'].includes(url.hostname)) { + if (url.hostname === 'maps.app.goo.gl') { + let resp = await fetch(urlString, {method: 'HEAD', redirect: 'manual'}); + if (!resp.headers.get('location')) { + return null + } + url = new URL(resp.headers.get('location')); + } + + if (!url.searchParams.has('q')) { + return null; + } + + let query = url.searchParams.get('q'); + let places = await forwardLookup(query, null, null); + if (places.length === 0) { + return null; + } + + let position = places[0].position; + return [position.lat, position.lng]; + } + + return null; +} \ No newline at end of file From 37050604b72b381ffdaa775c9e23312bfebc0cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20R=C4=85czy?= Date: Sun, 30 Jul 2023 23:55:39 -0700 Subject: [PATCH 3/7] Remove support for maps.app.goo.gl because of CORS --- src/utils/maps.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/utils/maps.js b/src/utils/maps.js index 94a9ca002..cc010fb2b 100644 --- a/src/utils/maps.js +++ b/src/utils/maps.js @@ -10,15 +10,7 @@ export async function coordinatesFromMapsUrl(urlString) { let ll = url.searchParams.get('ll'); let coords = ll.split(',').map((x) => parseFloat(x)); return coords; - } else if (['maps.google.com', 'google.com', 'maps.app.goo.gl'].includes(url.hostname)) { - if (url.hostname === 'maps.app.goo.gl') { - let resp = await fetch(urlString, {method: 'HEAD', redirect: 'manual'}); - if (!resp.headers.get('location')) { - return null - } - url = new URL(resp.headers.get('location')); - } - + } else if (['maps.google.com', 'google.com'].includes(url.hostname)) { if (!url.searchParams.has('q')) { return null; } From 1edbba068e92294922cbe83a57b86af56bb2e0fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20R=C4=85czy?= Date: Sun, 6 Aug 2023 23:42:50 -0700 Subject: [PATCH 4/7] Support google.com/maps links --- src/utils/maps.js | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/utils/maps.js b/src/utils/maps.js index cc010fb2b..910eb655f 100644 --- a/src/utils/maps.js +++ b/src/utils/maps.js @@ -2,7 +2,7 @@ import { forwardLookup } from "./geocode"; export async function coordinatesFromMapsUrl(urlString) { let url = new URL(urlString); - if (url.hostname === 'maps.apple.com') { + if (url.hostname.endsWith('maps.apple.com')) { if (!url.searchParams.has('ll')) { return null; } @@ -10,19 +10,26 @@ export async function coordinatesFromMapsUrl(urlString) { let ll = url.searchParams.get('ll'); let coords = ll.split(',').map((x) => parseFloat(x)); return coords; - } else if (['maps.google.com', 'google.com'].includes(url.hostname)) { - if (!url.searchParams.has('q')) { - return null; - } + } else if (['maps.google.com', 'google.com'].find((h) => url.hostname.endsWith(h))) { + if (url.searchParams.has('q')) { // maps.google.com?q=... + let query = url.searchParams.get('q'); + let places = await forwardLookup(query, null, null); + if (places.length === 0) { + return null; + } + + let position = places[0].position; + return position ? [position.lat, position.lng] : null; + } else { // google.com/maps/@..., google.com/maps/place/**/@... + let parts = url.pathname.split('/'); + let llPart = parts.find((p) => p.startsWith('@')); + if (!llPart) { + return null; + } - let query = url.searchParams.get('q'); - let places = await forwardLookup(query, null, null); - if (places.length === 0) { - return null; + let coords = llPart.slice(1).split(',').slice(0, 2).map((x) => parseFloat(x)); + return coords; } - - let position = places[0].position; - return [position.lat, position.lng]; } return null; From 0d05aa58604a65326df7ae8008ba20e6756cdf28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20R=C4=85czy?= Date: Sun, 6 Aug 2023 23:43:08 -0700 Subject: [PATCH 5/7] Unit tests for coordinatesFromMapsUrl --- src/utils/maps.test.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/utils/maps.test.js diff --git a/src/utils/maps.test.js b/src/utils/maps.test.js new file mode 100644 index 000000000..7169b24ae --- /dev/null +++ b/src/utils/maps.test.js @@ -0,0 +1,29 @@ +/* eslint-env jest */ +import { coordinatesFromMapsUrl } from './maps'; +import * as geocode from './geocode'; + +jest.mock('./geocode', () => ({ + forwardLookup: jest.fn() +})); + +describe('url parsing', () => { + it('should parse coordinates apple maps url', async () => { + let coords = await coordinatesFromMapsUrl('https://maps.apple.com/?ll=37.3319,-122.0312'); + expect(coords).toEqual([37.3319, -122.0312]); + }); + + it('should parse coordinates google.com/maps', async () => { + let coords1 = await coordinatesFromMapsUrl('https://www.google.com/maps/@37.3319,-122.0312,17z'); + expect(coords1).toEqual([37.3319, -122.0312]); + + let coords2 = await coordinatesFromMapsUrl('https://www.google.com/maps/place/Some+place+somewhere/@37.3319,-122.0312,17z'); + expect(coords2).toEqual([37.3319, -122.0312]); + }); + + it('should reverse lookup coordinates based of query of maps.google.com?q=', async () => { + geocode.forwardLookup.mockResolvedValue([{position: {lat: 37.3319, lng: -122.0312}}]); + + let coords = await coordinatesFromMapsUrl('https://www.google.com/maps?q=Some+place+somewhere'); + expect(coords).toEqual([37.3319, -122.0312]); + }); +}); From 49b0ab46949830705e92273bb7d3f39c75361ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20R=C4=85czy?= Date: Sun, 20 Aug 2023 16:35:37 -0700 Subject: [PATCH 6/7] Remove parsing logic of maps/place --- src/utils/maps.js | 11 ++--------- src/utils/maps.test.js | 8 -------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/utils/maps.js b/src/utils/maps.js index 910eb655f..b71445cfb 100644 --- a/src/utils/maps.js +++ b/src/utils/maps.js @@ -20,15 +20,8 @@ export async function coordinatesFromMapsUrl(urlString) { let position = places[0].position; return position ? [position.lat, position.lng] : null; - } else { // google.com/maps/@..., google.com/maps/place/**/@... - let parts = url.pathname.split('/'); - let llPart = parts.find((p) => p.startsWith('@')); - if (!llPart) { - return null; - } - - let coords = llPart.slice(1).split(',').slice(0, 2).map((x) => parseFloat(x)); - return coords; + } else { + return null; } } diff --git a/src/utils/maps.test.js b/src/utils/maps.test.js index 7169b24ae..18d5326e9 100644 --- a/src/utils/maps.test.js +++ b/src/utils/maps.test.js @@ -12,14 +12,6 @@ describe('url parsing', () => { expect(coords).toEqual([37.3319, -122.0312]); }); - it('should parse coordinates google.com/maps', async () => { - let coords1 = await coordinatesFromMapsUrl('https://www.google.com/maps/@37.3319,-122.0312,17z'); - expect(coords1).toEqual([37.3319, -122.0312]); - - let coords2 = await coordinatesFromMapsUrl('https://www.google.com/maps/place/Some+place+somewhere/@37.3319,-122.0312,17z'); - expect(coords2).toEqual([37.3319, -122.0312]); - }); - it('should reverse lookup coordinates based of query of maps.google.com?q=', async () => { geocode.forwardLookup.mockResolvedValue([{position: {lat: 37.3319, lng: -122.0312}}]); From c58757946577f7603a12f0a989946e02ffc2297f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20R=C4=85czy?= Date: Sun, 20 Aug 2023 16:38:18 -0700 Subject: [PATCH 7/7] Fix formatting --- src/url.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/url.js b/src/url.js index 75dd5546b..3dc56c975 100644 --- a/src/url.js +++ b/src/url.js @@ -59,8 +59,7 @@ export function getNavigationNav(pathname, search) { return { url: params.get('url'), } - } - else if (params.has('lat') && params.has('long')) { + } else if (params.has('lat') && params.has('long')) { return { lat: parseFloat(params.get('lat')), long: parseFloat(params.get('long')),