diff --git a/src/actions/history.js b/src/actions/history.js index 1cbc5bdb..414b3f2b 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, pushTimelineRange } from './index'; +import { getDongleID, getZoom, getPrimeNav, getClipsNav, getNavigationNav } from '../url'; +import { primeNav, selectDevice, pushTimelineRange, navigateToDestination } from './index'; import { clipsExit, fetchClipsDetails, fetchClipsList } from './clips'; export const onHistoryMiddleware = ({ dispatch, getState }) => (next) => (action) => { @@ -28,6 +28,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)); + } + 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 a0f8bac7..05f0e8f3 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; @@ -150,6 +151,40 @@ export function primeNav(nav, allowPathChange = true) { }; } +export function navigateToDestination(destination) { + return (dispatch, getState) => { + const state = getState(); + if (!state.dongleId) { + return; + } + + 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, + }); + } + }); + }; +} + export function fetchSharedDevice(dongleId) { return async (dispatch) => { try { diff --git a/src/actions/types.js b/src/actions/types.js index 06dd7b9a..d672e8c0 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -19,6 +19,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 a0646e2a..e330a263 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 ; } @@ -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 09835e62..e8ab0042 100644 --- a/src/components/Navigation/index.jsx +++ b/src/components/Navigation/index.jsx @@ -345,7 +345,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) @@ -354,6 +354,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, @@ -394,6 +400,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 d18f3e17..e497ab68 100644 --- a/src/reducers/globalState.js +++ b/src/reducers/globalState.js @@ -438,6 +438,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 c8dfea55..3dc56c97 100644 --- a/src/url.js +++ b/src/url.js @@ -48,3 +48,24 @@ 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('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')), + }; + } + return {}; + } + return null; +} \ No newline at end of file diff --git a/src/utils/maps.js b/src/utils/maps.js new file mode 100644 index 00000000..b71445cf --- /dev/null +++ b/src/utils/maps.js @@ -0,0 +1,29 @@ +import { forwardLookup } from "./geocode"; + +export async function coordinatesFromMapsUrl(urlString) { + let url = new URL(urlString); + if (url.hostname.endsWith('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'].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 { + return null; + } + } + + return null; +} \ No newline at end of file diff --git a/src/utils/maps.test.js b/src/utils/maps.test.js new file mode 100644 index 00000000..18d5326e --- /dev/null +++ b/src/utils/maps.test.js @@ -0,0 +1,21 @@ +/* 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 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]); + }); +});