diff --git a/src/components/dialogs/RouterDialog.tsx b/src/components/dialogs/RouterDialog.tsx index 0e11cd8..aa9510d 100644 --- a/src/components/dialogs/RouterDialog.tsx +++ b/src/components/dialogs/RouterDialog.tsx @@ -4,6 +4,7 @@ import * as EDITOR_ACTIONS from '~/redux/editor/actions'; import classnames from 'classnames'; import { connect } from 'react-redux'; import { selectEditor } from '~/redux/editor/selectors'; +import pick from 'ramda/es/pick'; const noPoints = ({ editorRouterCancel, @@ -80,7 +81,7 @@ const draggablePoints = ({ ); const mapStateToProps = state => ({ - editor: selectEditor(state), + editor: pick(['router'], selectEditor(state)), }); const mapDispatchToProps = { @@ -91,7 +92,10 @@ const mapDispatchToProps = { type Props = ReturnType & typeof mapDispatchToProps & { width?: number }; const RouterDialogUnconnected: FC = ({ - editor: { routerPoints, is_routing }, + editor: { + router: { waypoints }, + is_routing, + }, editorRouterCancel, editorRouterSubmit, width, @@ -99,9 +103,9 @@ const RouterDialogUnconnected: FC = ({
- {!routerPoints && noPoints({ editorRouterCancel })} - {routerPoints === 1 && firstPoint({ editorRouterCancel })} - {routerPoints >= 2 && draggablePoints({ editorRouterCancel, editorRouterSubmit })} + {!waypoints.length && noPoints({ editorRouterCancel })} + {waypoints.length === 1 && firstPoint({ editorRouterCancel })} + {waypoints.length >= 2 && draggablePoints({ editorRouterCancel, editorRouterSubmit })}
); diff --git a/src/containers/map/Map/index.tsx b/src/containers/map/Map/index.tsx index dbce2f3..fbb3897 100644 --- a/src/containers/map/Map/index.tsx +++ b/src/containers/map/Map/index.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { MainMap } from '~/constants/map'; -import { Map as MapInterface } from 'leaflet'; import { createPortal } from 'react-dom'; import { selectMapProvider, selectMapRoute, selectMapStickers } from '~/redux/map/selectors'; import { connect } from 'react-redux'; import * as MAP_ACTIONS from '~/redux/map/actions'; import { Route } from '~/containers/map/Route'; +import { Router } from '~/containers/map/Router'; import { TileLayer } from '~/containers/map/TileLayer'; import { Stickers } from '~/containers/map/Stickers'; @@ -71,6 +71,7 @@ const MapUnconnected: React.FC = ({ mapDropSticker={mapDropSticker} is_editing={editing} /> +
, document.getElementById('canvas') ); diff --git a/src/containers/map/Router/index.tsx b/src/containers/map/Router/index.tsx new file mode 100644 index 0000000..ad25411 --- /dev/null +++ b/src/containers/map/Router/index.tsx @@ -0,0 +1,61 @@ +import React, { FC, useEffect, useMemo, useCallback, memo } from 'react'; +import pick from 'ramda/es/pick'; +import { OsrmRouter } from '~/utils/osrm'; +import { connect } from 'react-redux'; +import { selectMap } from '~/redux/map/selectors'; +import { selectEditorRouter, selectEditorMode } from '~/redux/editor/selectors'; +import { MainMap } from '~/constants/map'; +import * as EDITOR_ACTIONS from '~/redux/editor/actions'; +import { MODES } from '~/constants/modes'; + +const mapStateToProps = state => ({ + map: pick(['route'], selectMap(state)), + router: pick(['waypoints', 'points'], selectEditorRouter(state)), + mode: selectEditorMode(state), +}); + +const mapDispatchToProps = { + editorSetRouter: EDITOR_ACTIONS.editorSetRouter, +}; + +type Props = ReturnType & typeof mapDispatchToProps & {}; + +const RouterUnconnected: FC = memo( + ({ map: { route }, mode, router: { waypoints }, editorSetRouter }) => { + const updateWaypoints = useCallback( + ({ waypoints }) => editorSetRouter({ waypoints: waypoints.filter(wp => !!wp.latLng) }), + [editorSetRouter] + ); + + useEffect(() => { + OsrmRouter.on('waypointschanged', updateWaypoints).addTo(MainMap); + + return () => { + OsrmRouter.off('waypointschanged', updateWaypoints).setWaypoints([]); + }; + }, [MainMap, updateWaypoints, mode]); + + useEffect(() => { + if (mode !== MODES.ROUTER) return; + + const wp = OsrmRouter.getWaypoints() + .filter(point => point.latLng) + .map(point => point.latLng); + + if ( + !route.length || + !wp.length || + (route[route.length - 1].lat === wp[0].lat && route[route.length - 1].lng === wp[0].lng) + ) + return; + + OsrmRouter.setWaypoints([route[route.length - 1], ...wp]); + }, [route, mode, waypoints]); + + return null; + } +); + +const Router = connect(mapStateToProps, mapDispatchToProps)(RouterUnconnected); + +export { Router }; diff --git a/src/redux/editor/actions.ts b/src/redux/editor/actions.ts index 8e72985..0132a02 100644 --- a/src/redux/editor/actions.ts +++ b/src/redux/editor/actions.ts @@ -118,3 +118,8 @@ export const editorKeyPressed = ({ key, target: tagName, }); + +export const editorSetRouter = (router: Partial) => ({ + type: EDITOR_ACTIONS.SET_ROUTER, + router, +}); diff --git a/src/redux/editor/constants.ts b/src/redux/editor/constants.ts index d0015a6..542b432 100644 --- a/src/redux/editor/constants.ts +++ b/src/redux/editor/constants.ts @@ -44,4 +44,6 @@ export const EDITOR_ACTIONS = { SET_FEATURE: `${P}-SET_FEATURE`, SET_IS_ROUTING: `${P}-SET_IS_ROUTING`, KEY_PRESSED: `${P}-KEY_PRESSED`, + + SET_ROUTER: `${P}-SET_ROUTER`, }; diff --git a/src/redux/editor/handlers.ts b/src/redux/editor/handlers.ts index 78c22a8..a3f8910 100644 --- a/src/redux/editor/handlers.ts +++ b/src/redux/editor/handlers.ts @@ -113,7 +113,10 @@ const resetSaveDialog = (state): IEditorState => ({ save_error: '', }); -const setDialog = (state, { dialog }: ReturnType): IEditorState => ({ +const setDialog = ( + state, + { dialog }: ReturnType +): IEditorState => ({ ...state, dialog, }); @@ -126,7 +129,10 @@ const setDialogActive = ( dialog_active: dialog_active || !state.dialog_active, }); -const setReady = (state, { ready = true }: ReturnType): IEditorState => ({ +const setReady = ( + state, + { ready = true }: ReturnType +): IEditorState => ({ ...state, ready, }); @@ -169,6 +175,17 @@ const setIsRouting = ( is_routing, }); +const setRouter = ( + state, + { router }: ReturnType +): IEditorState => ({ + ...state, + router: { + ...state.router, + ...router, + }, +}); + export const EDITOR_HANDLERS = { [EDITOR_ACTIONS.SET_EDITING]: setEditing, [EDITOR_ACTIONS.SET_CHANGED]: setChanged, @@ -197,4 +214,5 @@ export const EDITOR_HANDLERS = { [EDITOR_ACTIONS.SET_FEATURE]: setFeature, [EDITOR_ACTIONS.SET_IS_ROUTING]: setIsRouting, + [EDITOR_ACTIONS.SET_ROUTER]: setRouter, }; diff --git a/src/redux/editor/index.ts b/src/redux/editor/index.ts index 1bc7b0e..1024174 100644 --- a/src/redux/editor/index.ts +++ b/src/redux/editor/index.ts @@ -2,44 +2,51 @@ import { IDialogs } from '~/constants/dialogs'; import { MODES } from '~/constants/modes'; import { createReducer } from 'reduxsauce'; import { EDITOR_HANDLERS } from './handlers'; +import { ILatLng } from '../map/types'; export interface IEditorState { - changed: boolean, - editing: boolean, - ready: boolean, + changed: boolean; + editing: boolean; + ready: boolean; markers_shown: boolean; - - mode: typeof MODES[keyof typeof MODES], - dialog: IDialogs[keyof IDialogs], - dialog_active: boolean, + router: { + points: ILatLng[]; + waypoints: ILatLng[]; + }; - routerPoints: number, - distance: number, - estimated: number, - speed: number, - activeSticker: { set?: string, sticker?: string }, - is_empty: boolean, - is_published: boolean, - is_routing: boolean, - save_error: string, - save_finished: boolean, - save_overwriting: boolean, - save_processing: boolean, - save_loading: boolean, + mode: typeof MODES[keyof typeof MODES]; + + dialog: IDialogs[keyof IDialogs]; + dialog_active: boolean; + + routerPoints: number; + distance: number; + estimated: number; + speed: number; + activeSticker: { set?: string; sticker?: string }; + is_empty: boolean; + is_published: boolean; + is_routing: boolean; + + save_error: string; + save_finished: boolean; + save_overwriting: boolean; + save_processing: boolean; + save_loading: boolean; features: { - routing: boolean, - }, + routing: boolean; + }; renderer: { - data: string, - width: number, - height: number - renderer_active: boolean, - info: string, - progress: number, - }, + data: string; + width: number; + height: number; + renderer_active: boolean; + info: string; + progress: number; + }; } const EDITOR_INITIAL_STATE = { @@ -57,7 +64,10 @@ const EDITOR_INITIAL_STATE = { estimated: 0, speed: 15, activeSticker: { set: null, sticker: null }, - + router: { + waypoints: [], + points: [], + }, is_published: false, is_empty: true, is_routing: false, @@ -67,7 +77,7 @@ const EDITOR_INITIAL_STATE = { save_overwriting: false, save_processing: false, save_loading: false, - + features: { routing: false, }, @@ -80,6 +90,6 @@ const EDITOR_INITIAL_STATE = { info: '', progress: 0, }, -} +}; export const editor = createReducer(EDITOR_INITIAL_STATE, EDITOR_HANDLERS); diff --git a/src/redux/editor/sagas.ts b/src/redux/editor/sagas.ts index 79df798..7e2c4a6 100644 --- a/src/redux/editor/sagas.ts +++ b/src/redux/editor/sagas.ts @@ -1,7 +1,7 @@ import { call, put, takeEvery, takeLatest, select, race } from 'redux-saga/effects'; import { delay, SagaIterator } from 'redux-saga'; import { selectEditor } from '~/redux/editor/selectors'; - +import { simplify } from '~/utils/simplify'; import { editorHideRenderer, editorSetChanged, @@ -36,11 +36,15 @@ import { imageFetcher, downloadCanvas, } from '~/utils/renderer'; -import { selectMap } from '../map/selectors'; +import { selectMap, selectMapRoute } from '../map/selectors'; import { selectUser } from '../user/selectors'; import { LOGOS } from '~/constants/logos'; -import { loadMapSaga, replaceAddressIfItsBusy } from '../map/sagas'; -import { mapSetAddressOrigin } from '../map/actions'; +import { loadMapSaga } from '../map/sagas'; +import { mapClicked, mapSetRoute } from '../map/actions'; +import { MAP_ACTIONS } from '../map/constants'; +import { OsrmRouter } from '~/utils/osrm'; +import path from 'ramda/es/path'; +import { MainMap } from '~/constants/map'; const hideLoader = () => { document.getElementById('loader').style.opacity = String(0); @@ -230,10 +234,30 @@ function* getGPXTrackSaga(): SagaIterator { } function* routerCancel() { - yield put(editorSetMode(MODES.NONE)) + yield put(editorSetMode(MODES.NONE)); // TODO: clear router } +function* mapClick({ latlng }: ReturnType) { + const { mode }: ReturnType = yield select(selectEditor); + + if (mode === MODES.ROUTER) { + const wp = OsrmRouter.getWaypoints().filter(point => !!point.latLng); + OsrmRouter.setWaypoints([...wp, latlng]); + } +} + +function* routerSubmit() { + const route: ReturnType = yield select(selectMapRoute); + const latlngs = path(['_routes', 0, 'coordinates'], OsrmRouter); + + const coordinates = simplify({ map: MainMap, latlngs }); + + yield put(mapSetRoute([...route, ...coordinates])); + OsrmRouter.setWaypoints([]); + yield put(editorSetMode(MODES.NONE)); +} + export function* editorSaga() { yield takeEvery(EDITOR_ACTIONS.STOP_EDITING, stopEditingSaga); yield takeLatest(EDITOR_ACTIONS.TAKE_A_SHOT, takeAShotSaga); @@ -242,4 +266,6 @@ export function* editorSaga() { yield takeLatest(EDITOR_ACTIONS.KEY_PRESSED, keyPressedSaga); yield takeLatest(EDITOR_ACTIONS.GET_GPX_TRACK, getGPXTrackSaga); yield takeLatest(EDITOR_ACTIONS.ROUTER_CANCEL, routerCancel); + yield takeLatest(MAP_ACTIONS.MAP_CLICKED, mapClick); + yield takeLatest(EDITOR_ACTIONS.ROUTER_SUBMIT, routerSubmit); } diff --git a/src/redux/editor/selectors.ts b/src/redux/editor/selectors.ts index 725c9af..144a089 100644 --- a/src/redux/editor/selectors.ts +++ b/src/redux/editor/selectors.ts @@ -4,4 +4,5 @@ export const selectEditor = (state: IState) => state.editor; export const selectEditorEditing = (state: IState) => state.editor.editing; export const selectEditorMode = (state: IState) => state.editor.mode; export const selectEditorActiveSticker = (state: IState) => state.editor.activeSticker; -export const selectEditorRenderer = (state: IState) => state.editor.renderer; \ No newline at end of file +export const selectEditorRenderer = (state: IState) => state.editor.renderer; +export const selectEditorRouter = (state: IState) => state.editor.router; \ No newline at end of file diff --git a/src/utils/osrm.ts b/src/utils/osrm.ts new file mode 100644 index 0000000..fc5bcef --- /dev/null +++ b/src/utils/osrm.ts @@ -0,0 +1,65 @@ +import { Marker } from 'leaflet'; +import * as Routing from 'leaflet-routing-machine/src/index'; +import { CLIENT } from '~/config/frontend'; +import { DomMarker } from '~/utils/DomMarker'; +import { MainMap } from '~/constants/map'; + +const createWaypointMarker = (): DomMarker => { + const element = document.createElement('div'); + + // element.addEventListener('mousedown', this.lockPropagations); + // element.addEventListener('mouseup', this.unlockPropagations); + + return new DomMarker({ + element, + className: 'router-waypoint', + }); +}; + +const routeLine = r => + Routing.line(r, { + styles: [ + { color: 'white', opacity: 0.8, weight: 12 }, + { color: '#4597d0', opacity: 1, weight: 4, dashArray: '15,10' }, + ], + addWaypoints: true, + }); +// .on('linetouched', this.lockPropagations); + +export const OsrmRouter = Routing.control({ + serviceUrl: CLIENT.OSRM_URL, + profile: CLIENT.OSRM_PROFILE, + fitSelectedRoutes: false, + showAlternatives: false, + routeLine, + altLineOptions: { + styles: [{ color: '#4597d0', opacity: 1, weight: 3 }], + }, + show: false, + plan: Routing.plan([], { + createMarker: (i, wp) => { + const marker = new Marker(wp.latLng, { + draggable: true, + icon: createWaypointMarker(), + }) + .on('dragstart', () => MainMap.disableClicks()) + .on('dragend', () => MainMap.enableClicks()) + .on('contextmenu', ({ latlng }: any) => { + OsrmRouter.setWaypoints( + OsrmRouter.getWaypoints().filter( + point => + !point.latLng || (point.latLng.lat != latlng.lat && point.latLng.lng != latlng.lng) + ) + ); + }); + + return marker; + }, + routeWhileDragging: false, + }), + routeWhileDragging: false, + routingOptions: { + geometryOnly: false, + }, + useHints: false, +}); diff --git a/src/utils/simplify.ts b/src/utils/simplify.ts index 9e65df1..37f41ef 100644 --- a/src/utils/simplify.ts +++ b/src/utils/simplify.ts @@ -2,22 +2,8 @@ import { Map, LineUtil } from 'leaflet'; import { ILatLng } from "~/redux/map/types"; export const simplify = ({ map, latlngs }: { map: Map, latlngs: ILatLng[] }): ILatLng[] => { - const points = []; - const target = []; const zoom = 12; const mul = 0.7; // 0 - not simplifying, 1 - very rude. - // its better to estimate mul value by route length - - for (let i = 0; i < latlngs.length; i += 1) { - points.push(map.project({ lat: latlngs[i].lat, lng: latlngs[i].lng }, zoom)); - } - - const simplified = LineUtil.simplify(points, mul); - - // for (let i = 0; i < simplified.length; i += 1) { - // target.push(map.unproject(simplified[i], zoom)); - // } - // - // return target; - return simplified.map(item => map.unproject(item, zoom)); + const points = latlngs.map(({ lat, lng }) => map.project({ lat, lng }, zoom)); + return LineUtil.simplify(points, mul).map(item => map.unproject(item, zoom)); };