nominatim search (without working dialog)

This commit is contained in:
Fedor Katurov 2020-01-20 16:42:46 +07:00
parent c3e136cebb
commit b20a3445d1
27 changed files with 450 additions and 61 deletions

101
package-lock.json generated
View file

@ -775,6 +775,77 @@
"resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-0.2.0.tgz",
"integrity": "sha1-biWYB0SqIjMflLZFpULALT/P7pc=" "integrity": "sha1-biWYB0SqIjMflLZFpULALT/P7pc="
}, },
"@redux-saga/core": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.1.3.tgz",
"integrity": "sha512-8tInBftak8TPzE6X13ABmEtRJGjtK17w7VUs7qV17S8hCO5S3+aUTWZ/DBsBJPdE8Z5jOPwYALyvofgq1Ws+kg==",
"requires": {
"@babel/runtime": "7.8.3",
"@redux-saga/deferred": "1.1.2",
"@redux-saga/delay-p": "1.1.2",
"@redux-saga/is": "1.1.2",
"@redux-saga/symbols": "1.1.2",
"@redux-saga/types": "1.1.0",
"redux": "4.0.5",
"typescript-tuple": "2.2.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz",
"integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==",
"requires": {
"regenerator-runtime": "0.13.3"
}
},
"redux": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
"integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
"requires": {
"loose-envify": "1.4.0",
"symbol-observable": "1.2.0"
}
},
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
}
}
},
"@redux-saga/deferred": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.1.2.tgz",
"integrity": "sha512-908rDLHFN2UUzt2jb4uOzj6afpjgJe3MjICaUNO3bvkV/kN/cNeI9PMr8BsFXB/MR8WTAZQq/PlTq8Kww3TBSQ=="
},
"@redux-saga/delay-p": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.1.2.tgz",
"integrity": "sha512-ojc+1IoC6OP65Ts5+ZHbEYdrohmIw1j9P7HS9MOJezqMYtCDgpkoqB5enAAZrNtnbSL6gVCWPHaoaTY5KeO0/g==",
"requires": {
"@redux-saga/symbols": "1.1.2"
}
},
"@redux-saga/is": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.2.tgz",
"integrity": "sha512-OLbunKVsCVNTKEf2cH4TYyNbbPgvmZ52iaxBD4I1fTif4+MTXMa4/Z07L83zW/hTCXwpSZvXogqMqLfex2Tg6w==",
"requires": {
"@redux-saga/symbols": "1.1.2",
"@redux-saga/types": "1.1.0"
}
},
"@redux-saga/symbols": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.2.tgz",
"integrity": "sha512-EfdGnF423glv3uMwLsGAtE6bg+R9MdqlHEzExnfagXPrIiuxwr3bdiAwz3gi+PsrQ3yBlaBpfGLtDG8rf3LgQQ=="
},
"@redux-saga/types": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.1.0.tgz",
"integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg=="
},
"@types/classnames": { "@types/classnames": {
"version": "2.2.7", "version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.7.tgz",
@ -11242,9 +11313,12 @@
"integrity": "sha512-sSJAzNq7zka3qVHKce1hbvqf0Vf5DuTVm7dr4GtsqQVOexnrvbV47RWFiPxQ8fscnyiuWyD2O92DOxPl0tGCRg==" "integrity": "sha512-sSJAzNq7zka3qVHKce1hbvqf0Vf5DuTVm7dr4GtsqQVOexnrvbV47RWFiPxQ8fscnyiuWyD2O92DOxPl0tGCRg=="
}, },
"redux-saga": { "redux-saga": {
"version": "0.16.2", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-0.16.2.tgz", "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.3.tgz",
"integrity": "sha512-iIjKnRThI5sKPEASpUvySemjzwqwI13e3qP7oLub+FycCRDysLSAOwt958niZW6LhxfmS6Qm1BzbU70w/Koc4w==" "integrity": "sha512-RkSn/z0mwaSa5/xH/hQLo8gNf4tlvT18qXDNvedihLcfzh+jMchDgaariQoehCpgRltEm4zHKJyINEz6aqswTw==",
"requires": {
"@redux-saga/core": "1.1.3"
}
}, },
"reduxsauce": { "reduxsauce": {
"version": "1.0.1", "version": "1.0.1",
@ -13112,6 +13186,27 @@
"integrity": "sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==", "integrity": "sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==",
"dev": true "dev": true
}, },
"typescript-compare": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz",
"integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==",
"requires": {
"typescript-logic": "0.0.0"
}
},
"typescript-logic": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz",
"integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q=="
},
"typescript-tuple": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz",
"integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==",
"requires": {
"typescript-compare": "0.0.2"
}
},
"uglifyjs-webpack-plugin": { "uglifyjs-webpack-plugin": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz", "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz",

View file

@ -95,7 +95,7 @@
"reactrangeslider": "^3.0.6", "reactrangeslider": "^3.0.6",
"redux": "^4.0.1", "redux": "^4.0.1",
"redux-persist": "^5.10.0", "redux-persist": "^5.10.0",
"redux-saga": "^0.16.2", "redux-saga": "^1.0.0",
"reduxsauce": "^1.0.0", "reduxsauce": "^1.0.0",
"scrypt": "^6.0.3", "scrypt": "^6.0.3",
"throttle-debounce": "^2.1.0", "throttle-debounce": "^2.1.0",

View file

@ -0,0 +1,16 @@
import React, { FC } from 'react';
import { Icon } from '~/components/panels/Icon';
interface IProps {}
const DialogLoader: FC<IProps> = ({}) => {
return (
<div className="dialog-maplist-loader">
<div className="dialog-maplist-icon spin">
<Icon icon="icon-sync-1" />
</div>
</div>
);
};
export { DialogLoader };

View file

@ -24,6 +24,7 @@ import { IRouteListItem } from '~/redux/user';
import { ROLES } from '~/constants/auth'; import { ROLES } from '~/constants/auth';
import { IState } from '~/redux/store'; import { IState } from '~/redux/store';
import { MapListDialogHead } from '~/components/search/MapListDialogHead'; import { MapListDialogHead } from '~/components/search/MapListDialogHead';
import { DialogLoader } from '~/components/dialogs/DialogLoader';
const mapStateToProps = ({ const mapStateToProps = ({
editor: { editing }, editor: { editing },
@ -165,11 +166,7 @@ class MapListDialogUnconnected extends PureComponent<Props, State> {
return ( return (
<div className="dialog-content full"> <div className="dialog-content full">
{list.length === 0 && loading && ( {list.length === 0 && loading && (
<div className="dialog-maplist-loader"> <DialogLoader />
<div className="dialog-maplist-icon spin">
<Icon icon="icon-sync-1" />
</div>
</div>
)} )}
{ready && !loading && list.length === 0 && ( {ready && !loading && list.length === 0 && (

View file

@ -0,0 +1,46 @@
import React, { FC, Fragment, useCallback } from 'react';
import { connect } from 'react-redux';
import { IState } from '~/redux/store';
import { selectEditorNominatim } from '~/redux/editor/selectors';
import { DialogLoader } from './DialogLoader';
import { NominatimListItem } from '~/components/nominatim/NominatimListItem';
import { MainMap } from '~/constants/map';
import { Scroll } from '../Scroll';
const mapStateToProps = (state: IState) => ({
nominatim: selectEditorNominatim(state),
});
type Props = ReturnType<typeof mapStateToProps> & {};
const NominatimDialogUnconnected: FC<Props> = ({ nominatim: { loading, list } }) => {
const onItemClick = useCallback(
(index: number) => {
if (!list[index]) return;
MainMap.setView(list[index].latlng, 17);
},
[MainMap, list]
);
return (
<Fragment>
<div style={{ flex: 1 }} />
<div className="dialog-flex-scroll">
<Scroll>
<div className="dialog-content nominatim-dialog-content">
{loading && <DialogLoader />}
{list.map((item, i) => (
<NominatimListItem item={item} key={item.id} />
))}
</div>
</Scroll>
</div>
</Fragment>
);
};
const NominatimDialog = connect(mapStateToProps)(NominatimDialogUnconnected);
export { NominatimDialog };

View file

@ -0,0 +1,37 @@
import React, { FC, useCallback, useState } from 'react';
import { Icon } from '../panels/Icon';
interface IProps {
active: boolean;
onSearch: (search: string) => void;
}
const NominatimSearchPanel: FC<IProps> = ({ active, onSearch }) => {
const [search, setSearch] = useState('Колывань');
const setValue = useCallback(({ target: { value } }) => setSearch(value), [setSearch]);
const onSubmit = useCallback(event => {
event.preventDefault();
if (search.length < 3) return;
onSearch(search);
}, [search, onSearch]);
return (
<form className="panel nominatim-panel active" onSubmit={onSubmit}>
<div className="control-bar">
<div className="nominatim-search-input">
<input type="text" placeholder="Поиск на карте" value={search} onChange={setValue} />
</div>
<button>
<Icon icon="icon-search" />
</button>
</div>
</form>
);
};
export { NominatimSearchPanel };

View file

@ -0,0 +1,21 @@
import React, { FC, useCallback } from 'react';
import { INominatimResult } from '~/redux/types';
import { MainMap } from '~/constants/map';
interface IProps {
item: INominatimResult;
}
const NominatimListItem: FC<IProps> = ({ item }) => {
const onClick = useCallback(() => {
MainMap.panTo(item.latlng);
}, [MainMap]);
return (
<div onClick={onClick} className="nominatim-list-item">
<div className="title">{item.title}</div>
</div>
);
};
export { NominatimListItem };

View file

@ -32,7 +32,7 @@ type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {}
class EditorPanelUnconnected extends PureComponent<Props, void> { class EditorPanelUnconnected extends PureComponent<Props, void> {
componentDidMount() { componentDidMount() {
window.addEventListener('keydown', this.props.editorKeyPressed as any); window.addEventListener('keydown', this.onKeyPress as any);
const obj = document.getElementById('control-dialog'); const obj = document.getElementById('control-dialog');
const { width } = this.panel.getBoundingClientRect(); const { width } = this.panel.getBoundingClientRect();
@ -45,9 +45,15 @@ class EditorPanelUnconnected extends PureComponent<Props, void> {
panel: HTMLElement = null; panel: HTMLElement = null;
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('keydown', this.props.editorKeyPressed as any); window.removeEventListener('keydown', this.onKeyPress as any);
} }
onKeyPress = event => {
if (event.target.tagName === 'TEXTAREA' || event.target.tagName === 'INPUT') return;
this.props.editorKeyPressed(event);
};
startPolyMode = () => this.props.editorSetMode(MODES.POLY); startPolyMode = () => this.props.editorSetMode(MODES.POLY);
startStickerMode = () => this.props.editorSetMode(MODES.STICKERS_SELECT); startStickerMode = () => this.props.editorSetMode(MODES.STICKERS_SELECT);
startRouterMode = () => this.props.editorSetMode(MODES.ROUTER); startRouterMode = () => this.props.editorSetMode(MODES.ROUTER);

View file

@ -10,6 +10,7 @@ import {
editorSetDialog, editorSetDialog,
editorSetDialogActive, editorSetDialogActive,
editorGetGPXTrack, editorGetGPXTrack,
editorSearchNominatim,
} from '~/redux/editor/actions'; } from '~/redux/editor/actions';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Icon } from '~/components/panels/Icon'; import { Icon } from '~/components/panels/Icon';
@ -19,11 +20,12 @@ import { CLIENT } from '~/config/frontend';
import { DIALOGS, TABS } from '~/constants/dialogs'; import { DIALOGS, TABS } from '~/constants/dialogs';
import { Tooltip } from '~/components/panels/Tooltip'; import { Tooltip } from '~/components/panels/Tooltip';
import { TitleDialog } from '~/components/dialogs/TitleDialog'; import { TitleDialog } from '~/components/dialogs/TitleDialog';
import { NominatimSearchPanel } from '~/components/dialogs/NominatimSearchPanel';
import { IState } from '~/redux/store'; import { IState } from '~/redux/store';
const mapStateToProps = ({ const mapStateToProps = ({
user: { user }, user: { user },
editor: { dialog, dialog_active }, editor: { dialog, dialog_active, features },
map: { route, stickers }, map: { route, stickers },
}: IState) => ({ }: IState) => ({
dialog, dialog,
@ -31,6 +33,7 @@ const mapStateToProps = ({
user, user,
route, route,
stickers, stickers,
features,
}); });
const mapDispatchToProps = { const mapDispatchToProps = {
@ -42,6 +45,7 @@ const mapDispatchToProps = {
editorSetDialogActive, editorSetDialogActive,
openMapDialog, openMapDialog,
editorGetGPXTrack, editorGetGPXTrack,
editorSearchNominatim,
}; };
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {}; type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
@ -90,6 +94,7 @@ export class UserPanelUnconnected extends PureComponent<Props, State> {
} }
setMenuOpened = () => this.setState({ menuOpened: !this.state.menuOpened }); setMenuOpened = () => this.setState({ menuOpened: !this.state.menuOpened });
openMapsDialog = () => { openMapsDialog = () => {
this.props.openMapDialog(TABS.MY); this.props.openMapDialog(TABS.MY);
}; };
@ -115,7 +120,7 @@ export class UserPanelUnconnected extends PureComponent<Props, State> {
render() { render() {
const { const {
props: { user, dialog, dialog_active, route, stickers }, props: { user, dialog, dialog_active, route, stickers, features },
state: { menuOpened }, state: { menuOpened },
} = this; } = this;
@ -124,6 +129,7 @@ export class UserPanelUnconnected extends PureComponent<Props, State> {
return ( return (
<div> <div>
<TitleDialog /> <TitleDialog />
<NominatimSearchPanel active={features.nominatim} onSearch={this.props.editorSearchNominatim} />
<div className="panel active panel-user"> <div className="panel active panel-user">
<div className="user-panel"> <div className="user-panel">

View file

@ -1,22 +1,11 @@
export interface IDialogs { export const DIALOGS = ({
NONE: string,
MAP_LIST: string,
APP_INFO: string,
}
export interface IMapTabs {
MY: string,
PENDING: string,
STARRED: string,
}
export const DIALOGS: IDialogs = ({
NONE: 'NONE', NONE: 'NONE',
MAP_LIST: 'MAP_LIST', MAP_LIST: 'MAP_LIST',
APP_INFO: 'APP_INFO', APP_INFO: 'APP_INFO',
NOMINATIM: 'NOMINATIM',
}); });
export const TABS: IMapTabs = { export const TABS = {
MY: 'my', MY: 'my',
PENDING: 'pending', PENDING: 'pending',
STARRED: 'starred', STARRED: 'starred',

View file

@ -4,7 +4,6 @@ import { EditorPanel } from '~/components/panels/EditorPanel';
import { Fills } from '~/components/Fills'; import { Fills } from '~/components/Fills';
import { UserPanel } from '~/components/panels/UserPanel'; import { UserPanel } from '~/components/panels/UserPanel';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import { Renderer } from '~/components/renderer/Renderer'; import { Renderer } from '~/components/renderer/Renderer';
@ -14,7 +13,7 @@ import { TopLeftPanel } from '~/components/panels/TopLeftPanel';
import { TopRightPanel } from '~/components/panels/TopRightPanel'; import { TopRightPanel } from '~/components/panels/TopRightPanel';
import { LogoPreview } from '~/components/logo/LogoPreview'; import { LogoPreview } from '~/components/logo/LogoPreview';
import { IStickerPack } from '~/constants/stickers'; import { IStickerPack } from '~/constants/stickers';
import { IDialogs } from '~/constants/dialogs'; import { DIALOGS } from '~/constants/dialogs';
import { Map } from '~/map/Map'; import { Map } from '~/map/Map';
import { IEditorState } from '~/redux/editor'; import { IEditorState } from '~/redux/editor';
@ -25,7 +24,7 @@ type Props = {
renderer_active: boolean; renderer_active: boolean;
mode: IEditorState['mode']; mode: IEditorState['mode'];
dialog: keyof IDialogs; dialog: keyof typeof DIALOGS;
dialog_active: boolean; dialog_active: boolean;
set: keyof IStickerPack; set: keyof IStickerPack;
editorHideRenderer: typeof editorHideRenderer; editorHideRenderer: typeof editorHideRenderer;

View file

@ -1,13 +1,14 @@
import React, { createElement, FC, memo } from 'react'; import React, { createElement, FC, memo } from 'react';
import { DIALOGS, IDialogs } from '~/constants/dialogs'; import { DIALOGS } from '~/constants/dialogs';
import classnames from 'classnames'; import classnames from 'classnames';
import { AppInfoDialog } from '~/components/dialogs/AppInfoDialog'; import { AppInfoDialog } from '~/components/dialogs/AppInfoDialog';
import { Icon } from '~/components/panels/Icon'; import { Icon } from '~/components/panels/Icon';
import { MapListDialog } from '~/components/dialogs/MapListDialog'; import { MapListDialog } from '~/components/dialogs/MapListDialog';
import { NominatimDialog } from '~/components/dialogs/NominatimDialog';
import * as EDITOR_ACTIONS from '~/redux/editor/actions'; import * as EDITOR_ACTIONS from '~/redux/editor/actions';
interface Props { interface Props {
dialog: keyof IDialogs; dialog: keyof typeof DIALOGS;
dialog_active: Boolean; dialog_active: Boolean;
editorSetDialogActive: typeof EDITOR_ACTIONS.editorSetDialogActive; editorSetDialogActive: typeof EDITOR_ACTIONS.editorSetDialogActive;
} }
@ -15,6 +16,7 @@ interface Props {
const LEFT_DIALOGS = { const LEFT_DIALOGS = {
[DIALOGS.MAP_LIST]: MapListDialog, [DIALOGS.MAP_LIST]: MapListDialog,
[DIALOGS.APP_INFO]: AppInfoDialog, [DIALOGS.APP_INFO]: AppInfoDialog,
[DIALOGS.NOMINATIM]: NominatimDialog,
}; };
const LeftDialog: FC<Props> = memo(({ dialog, dialog_active, editorSetDialogActive }) => ( const LeftDialog: FC<Props> = memo(({ dialog, dialog_active, editorSetDialogActive }) => (

View file

@ -51,8 +51,12 @@ const MapUnconnected: React.FC<IProps> = ({
}) => { }) => {
const onClick = React.useCallback( const onClick = React.useCallback(
event => { event => {
if (!MainMap.clickable || mode === MODES.NONE) return; if (
!MainMap.clickable ||
mode === MODES.NONE
)
return;
mapClicked(event.latlng); mapClicked(event.latlng);
}, },
[mapClicked, mode] [mapClicked, mode]

View file

@ -112,3 +112,13 @@ export const editorSetRouter = (router: Partial<IEditorState['router']>) => ({
type: EDITOR_ACTIONS.SET_ROUTER, type: EDITOR_ACTIONS.SET_ROUTER,
router, router,
}); });
export const editorSetNominatim = (nominatim: Partial<IEditorState['nominatim']>) => ({
type: EDITOR_ACTIONS.SET_NOMINATIM,
nominatim,
})
export const editorSearchNominatim = (search: IEditorState['nominatim']['search']) => ({
type: EDITOR_ACTIONS.SEARCH_NOMINATIM,
search,
})

View file

@ -44,4 +44,6 @@ export const EDITOR_ACTIONS = {
KEY_PRESSED: `${P}-KEY_PRESSED`, KEY_PRESSED: `${P}-KEY_PRESSED`,
SET_ROUTER: `${P}-SET_ROUTER`, SET_ROUTER: `${P}-SET_ROUTER`,
SET_NOMINATIM: `${P}-SET_NOMINATIM`,
SEARCH_NOMINATIM: `${P}-SEARCH_NOMINATIM`,
}; };

View file

@ -157,6 +157,17 @@ const setRouter = (
}, },
}); });
const setNominatim = (
state,
{ nominatim }: ReturnType<typeof ACTIONS.editorSetNominatim>
): IEditorState => ({
...state,
nominatim: {
...state.nominatim,
...nominatim,
},
});
export const EDITOR_HANDLERS = { export const EDITOR_HANDLERS = {
[EDITOR_ACTIONS.SET_EDITING]: setEditing, [EDITOR_ACTIONS.SET_EDITING]: setEditing,
[EDITOR_ACTIONS.SET_CHANGED]: setChanged, [EDITOR_ACTIONS.SET_CHANGED]: setChanged,
@ -184,4 +195,5 @@ export const EDITOR_HANDLERS = {
[EDITOR_ACTIONS.SET_FEATURE]: setFeature, [EDITOR_ACTIONS.SET_FEATURE]: setFeature,
[EDITOR_ACTIONS.SET_IS_ROUTING]: setIsRouting, [EDITOR_ACTIONS.SET_IS_ROUTING]: setIsRouting,
[EDITOR_ACTIONS.SET_ROUTER]: setRouter, [EDITOR_ACTIONS.SET_ROUTER]: setRouter,
[EDITOR_ACTIONS.SET_NOMINATIM]: setNominatim,
}; };

View file

@ -1,8 +1,9 @@
import { createReducer } from '~/utils/reducer'; import { createReducer } from '~/utils/reducer';
import { IDialogs } from '~/constants/dialogs'; import { DIALOGS } from '~/constants/dialogs';
import { MODES } from '~/constants/modes'; import { MODES } from '~/constants/modes';
import { EDITOR_HANDLERS } from './handlers'; import { EDITOR_HANDLERS } from './handlers';
import { ILatLng } from '../map/types'; import { ILatLng } from '../map/types';
import { INominatimResult } from '~/redux/types';
export interface IEditorState { export interface IEditorState {
changed: boolean; changed: boolean;
@ -11,13 +12,13 @@ export interface IEditorState {
markers_shown: boolean; markers_shown: boolean;
router: { router: {
points: ILatLng[]; points: ILatLng[];
waypoints: ILatLng[]; waypoints: ILatLng[];
}; };
mode: typeof MODES[keyof typeof MODES]; mode: typeof MODES[keyof typeof MODES];
dialog: IDialogs[keyof IDialogs]; dialog: typeof DIALOGS[keyof typeof DIALOGS];
dialog_active: boolean; dialog_active: boolean;
routerPoints: number; routerPoints: number;
@ -31,6 +32,13 @@ export interface IEditorState {
features: { features: {
routing: boolean; routing: boolean;
nominatim: boolean;
};
nominatim: {
search: string;
loading: boolean;
list: INominatimResult[];
}; };
renderer: { renderer: {
@ -76,6 +84,13 @@ export const EDITOR_INITIAL_STATE = {
features: { features: {
routing: false, routing: false,
nominatim: false,
},
nominatim: {
search: '',
loading: false,
list: [],
}, },
renderer: { renderer: {

View file

@ -1,5 +1,13 @@
import { call, put, takeEvery, takeLatest, select, race } from 'redux-saga/effects'; import {
import { delay, SagaIterator } from 'redux-saga'; call,
put,
takeEvery,
takeLatest,
select,
race,
takeLeading,
delay,
} from 'redux-saga/effects';
import { selectEditor, selectEditorMode } from '~/redux/editor/selectors'; import { selectEditor, selectEditorMode } from '~/redux/editor/selectors';
import { simplify } from '~/utils/simplify'; import { simplify } from '~/utils/simplify';
import { import {
@ -14,13 +22,16 @@ import {
editorLocationChanged, editorLocationChanged,
editorKeyPressed, editorKeyPressed,
editorSetSave, editorSetSave,
editorSearchNominatim,
editorSetDialog,
editorSetNominatim,
} from '~/redux/editor/actions'; } from '~/redux/editor/actions';
import { getUrlData, pushPath } from '~/utils/history'; import { getUrlData, pushPath } from '~/utils/history';
import { MODES } from '~/constants/modes'; import { MODES } from '~/constants/modes';
import { checkOSRMService } from '~/utils/api'; import { checkOSRMService, checkNominatimService, searchNominatim } from '~/utils/api';
import { LatLng } from 'leaflet'; import { LatLng } from 'leaflet';
import { searchSetTab } from '../user/actions'; import { searchSetTab } from '../user/actions';
import { TABS } from '~/constants/dialogs'; import { TABS, DIALOGS } from '~/constants/dialogs';
import { EDITOR_ACTIONS } from './constants'; import { EDITOR_ACTIONS } from './constants';
import { getGPXString, downloadGPXTrack } from '~/utils/gpx'; import { getGPXString, downloadGPXTrack } from '~/utils/gpx';
import { import {
@ -76,11 +87,17 @@ function* checkOSRMServiceSaga() {
yield put(editorSetFeature({ routing })); yield put(editorSetFeature({ routing }));
} }
function* checkNominatimSaga() {
const nominatim = yield call(checkNominatimService);
yield put(editorSetFeature({ nominatim }));
}
export function* setReadySaga() { export function* setReadySaga() {
yield put(editorSetReady(true)); yield put(editorSetReady(true));
hideLoader(); hideLoader();
yield call(checkOSRMServiceSaga); yield call(checkOSRMServiceSaga);
yield call(checkNominatimSaga);
yield put(searchSetTab(TABS.MY)); yield put(searchSetTab(TABS.MY));
} }
@ -217,7 +234,7 @@ function* keyPressedSaga({ key, target }: ReturnType<typeof editorKeyPressed>) {
} }
} }
function* getGPXTrackSaga(): SagaIterator { function* getGPXTrackSaga() {
const { route, stickers, title, address }: ReturnType<typeof selectMap> = yield select(selectMap); const { route, stickers, title, address }: ReturnType<typeof selectMap> = yield select(selectMap);
if (!route.length && !stickers.length) return; if (!route.length && !stickers.length) return;
@ -259,6 +276,22 @@ function* cancelSave() {
); );
} }
function* searchNominatimSaga({ search }: ReturnType<typeof editorSearchNominatim>) {
const { dialog, dialog_active }: ReturnType<typeof selectEditor> = yield select(selectEditor);
if (dialog !== DIALOGS.NOMINATIM || !dialog_active) {
yield put(editorSetDialog(DIALOGS.NOMINATIM));
yield put(editorSetDialogActive(true));
}
yield put(editorSetNominatim({ loading: true, search }));
const list = yield call(searchNominatim, search);
yield put(editorSetNominatim({ list }));
yield delay(1000); // safely wait for 1s to prevent from ddosing nominatim
yield put(editorSetNominatim({ loading: false }));
}
export function* editorSaga() { export function* editorSaga() {
yield takeEvery(EDITOR_ACTIONS.LOCATION_CHANGED, locationChangeSaga); yield takeEvery(EDITOR_ACTIONS.LOCATION_CHANGED, locationChangeSaga);
@ -271,4 +304,5 @@ export function* editorSaga() {
yield takeLatest(MAP_ACTIONS.MAP_CLICKED, mapClick); yield takeLatest(MAP_ACTIONS.MAP_CLICKED, mapClick);
yield takeLatest(EDITOR_ACTIONS.ROUTER_SUBMIT, routerSubmit); yield takeLatest(EDITOR_ACTIONS.ROUTER_SUBMIT, routerSubmit);
yield takeLatest(EDITOR_ACTIONS.CANCEL_SAVE, cancelSave); yield takeLatest(EDITOR_ACTIONS.CANCEL_SAVE, cancelSave);
yield takeLeading(EDITOR_ACTIONS.SEARCH_NOMINATIM, searchNominatimSaga);
} }

View file

@ -8,3 +8,4 @@ export const selectEditorActiveSticker = (state: IState) => state.editor.activeS
export const selectEditorRenderer = (state: IState) => state.editor.renderer; export const selectEditorRenderer = (state: IState) => state.editor.renderer;
export const selectEditorRouter = (state: IState) => state.editor.router; export const selectEditorRouter = (state: IState) => state.editor.router;
export const selectEditorDistance = (state: IState) => state.editor.distance; export const selectEditorDistance = (state: IState) => state.editor.distance;
export const selectEditorNominatim = (state: IState) => state.editor.nominatim;

View file

@ -7,6 +7,7 @@ import {
race, race,
take, take,
takeLatest, takeLatest,
delay,
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import { MAP_ACTIONS } from './constants'; import { MAP_ACTIONS } from './constants';
import { import {
@ -36,7 +37,6 @@ import { getStoredMap, postMap } from '~/utils/api';
import { Unwrap } from '~/utils/middleware'; import { Unwrap } from '~/utils/middleware';
import { selectMap, selectMapProvider, selectMapRoute, selectMapStickers } from './selectors'; import { selectMap, selectMapProvider, selectMapRoute, selectMapStickers } from './selectors';
import { TIPS } from '~/constants/tips'; import { TIPS } from '~/constants/tips';
import { delay } from 'redux-saga';
import { setReadySaga } from '../editor/sagas'; import { setReadySaga } from '../editor/sagas';
import { selectEditor } from '../editor/selectors'; import { selectEditor } from '../editor/selectors';
import { EDITOR_ACTIONS } from '../editor/constants'; import { EDITOR_ACTIONS } from '../editor/constants';

View file

@ -0,0 +1,7 @@
import { LatLngLiteral } from 'leaflet';
export interface INominatimResult {
id: number;
title: string;
latlng: LatLngLiteral;
};

View file

@ -1,6 +1,5 @@
import { REHYDRATE, RehydrateAction } from 'redux-persist'; import { REHYDRATE, RehydrateAction } from 'redux-persist';
import { delay, SagaIterator } from 'redux-saga'; import { takeLatest, select, call, put, takeEvery, delay } from 'redux-saga/effects';
import { takeLatest, select, call, put, takeEvery } from 'redux-saga/effects';
import { import {
checkIframeToken, checkIframeToken,
checkUserToken, checkUserToken,
@ -34,7 +33,6 @@ import { selectUser, selectUserUser } from './selectors';
import { mapInitSaga } from '~/redux/map/sagas'; import { mapInitSaga } from '~/redux/map/sagas';
import { editorSetDialog, editorSetDialogActive } from '../editor/actions'; import { editorSetDialog, editorSetDialogActive } from '../editor/actions';
import { selectEditor } from '../editor/selectors'; import { selectEditor } from '../editor/selectors';
import { getLocation, watchLocation } from '~/utils/window';
function* generateGuestSaga() { function* generateGuestSaga() {
const { const {
@ -198,7 +196,7 @@ function* searchSetTabSaga() {
yield put(searchSetTitle('')); yield put(searchSetTitle(''));
} }
function* userLogoutSaga(): SagaIterator { function* userLogoutSaga() {
yield put(setUser(DEFAULT_USER)); yield put(setUser(DEFAULT_USER));
yield call(generateGuestSaga); yield call(generateGuestSaga);
} }
@ -256,7 +254,7 @@ function* mapsLoadMoreSaga() {
yield put(searchSetLoading(false)); yield put(searchSetLoading(false));
} }
function* dropRouteSaga({ address }: ReturnType<typeof ActionCreators.dropRoute>): SagaIterator { function* dropRouteSaga({ address }: ReturnType<typeof ActionCreators.dropRoute>) {
const { token }: ReturnType<typeof selectUserUser> = yield select(selectUserUser); const { token }: ReturnType<typeof selectUserUser> = yield select(selectUserUser);
const { const {
routes: { routes: {
@ -290,7 +288,7 @@ function* modifyRouteSaga({
address, address,
title, title,
is_public, is_public,
}: ReturnType<typeof ActionCreators.modifyRoute>): SagaIterator { }: ReturnType<typeof ActionCreators.modifyRoute>) {
const { token }: ReturnType<typeof selectUserUser> = yield select(selectUserUser); const { token }: ReturnType<typeof selectUserUser> = yield select(selectUserUser);
const { const {
routes: { routes: {

View file

@ -422,6 +422,13 @@
<path d="M12 14c-2.33 0-4.32 1.45-5.12 3.5h1.67c.69-1.19 1.97-2 3.45-2s2.75.81 3.45 2h1.67c-.8-2.05-2.79-3.5-5.12-3.5zm-.01-12C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" fill="white" stroke="none" stroke-width="0"/> <path d="M12 14c-2.33 0-4.32 1.45-5.12 3.5h1.67c.69-1.19 1.97-2 3.45-2s2.75.81 3.45 2h1.67c-.8-2.05-2.79-3.5-5.12-3.5zm-.01-12C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" fill="white" stroke="none" stroke-width="0"/>
</g> </g>
</g> </g>
<g id="icon-search" stroke="none">
<path stroke="none" fill="black"/>
<g transform="translate(4 4)">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
</g>
</g>
</svg> </svg>
</defs> </defs>

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

View file

@ -36,7 +36,6 @@
background: rgba(19, 45, 53, 0.95); background: rgba(19, 45, 53, 0.95);
} }
} }
} }
.dialog-close-button { .dialog-close-button {
@ -94,8 +93,41 @@
} }
} }
.dialog-flex-scroll {
display: flex;
align-items: center;
justify-content: center;
}
.nominatim-dialog-content {
padding-bottom: 48px;
min-height: 25vh;
}
.nominatim-list-item {
padding: 10px;
color: white;
cursor: pointer;
transition: background-color 0.25s;
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 200px;
&:hover {
background: rgba(255, 255, 255, 0.1);
}
.title {
text-overflow: hidden;
font-size: 12px;
-webkit-line-clamp: 2;
}
}
.dialog-shader { .dialog-shader {
&::before, &::after { &::before,
&::after {
content: ' '; content: ' ';
height: 40px; height: 40px;
width: 100%; width: 100%;
@ -120,13 +152,21 @@
} }
@keyframes pulse { @keyframes pulse {
0% { opacity: 1; } 0% {
100% { opacity: 0.5; } opacity: 1;
}
100% {
opacity: 0.5;
}
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
} }
.dialog-maplist-pulse { .dialog-maplist-pulse {
@ -195,11 +235,14 @@
&.has_edit { &.has_edit {
//transform: translateY(-2px); //transform: translateY(-2px);
.route-row { background: fade(@green_secondary, 30%); } .route-row {
background: fade(@green_secondary, 30%);
}
} }
&.is_menu_target { &.is_menu_target {
.route-row, .route-row-fav { .route-row,
.route-row-fav {
transform: translateX(-120px); transform: translateX(-120px);
} }
@ -337,7 +380,6 @@
} }
} }
} }
} }
.route-title { .route-title {
@ -414,7 +456,7 @@
} }
} }
@media(max-width: @mobile_breakpoint) { @media (max-width: @mobile_breakpoint) {
height: 48px; height: 48px;
.dialog-tab { .dialog-tab {

View file

@ -3,6 +3,8 @@
border-radius: @panel_radius; border-radius: @panel_radius;
display: flex; display: flex;
box-shadow: @bar_shadow; box-shadow: @bar_shadow;
align-items: center;
justify-content: center;
@media (max-width: @mobile_breakpoint) { @media (max-width: @mobile_breakpoint) {
box-shadow: none; box-shadow: none;
@ -723,4 +725,16 @@
.location-bar { .location-bar {
width: 32px; width: 32px;
}
.nominatim-panel {
position: fixed;
bottom: 53px;
left: 10px;
width: 272px
}
.nominatim-search-input {
padding-left: 10px;
flex: 1;
} }

View file

@ -11,6 +11,7 @@ import {
configWithToken, configWithToken,
} from './middleware'; } from './middleware';
import { IRoute } from '~/redux/map/types'; import { IRoute } from '~/redux/map/types';
import { INominatimResult } from '~/redux/types';
const arrayToObject = (array: any[], key: string): {} => const arrayToObject = (array: any[], key: string): {} =>
array.reduce((obj, el) => ({ ...obj, [el[key]]: el }), {}); array.reduce((obj, el) => ({ ...obj, [el[key]]: el }), {});
@ -180,6 +181,36 @@ export const checkOSRMService = (bounds: LatLngLiteral[]): Promise<boolean> =>
.then(() => true) .then(() => true)
.catch(() => false); .catch(() => false);
export const checkNominatimService = (): Promise<boolean> =>
CLIENT &&
CLIENT.NOMINATIM_TEST_URL &&
axios
.get(CLIENT.NOMINATIM_TEST_URL)
.then(() => true)
.catch(() => false);
export const searchNominatim = (query: string) =>
CLIENT &&
CLIENT.NOMINATIM_URL &&
axios
.get(`${CLIENT.NOMINATIM_URL}${query}`, { params: { format: 'json', country_code: 'ru', 'accept-language': 'ru_RU' } })
.then(
data =>
data &&
data.data &&
data.data.map(
(item): INominatimResult => ({
id: item.place_id,
latlng: {
lat: item.lat,
lng: item.lon,
},
title: item.display_name,
})
)
)
.catch(() => []);
export const dropRoute = ({ address, token }: { address: string; token: string }): Promise<any> => export const dropRoute = ({ address, token }: { address: string; token: string }): Promise<any> =>
axios axios
.delete(API.DROP_ROUTE, configWithToken(token, { data: { address } })) .delete(API.DROP_ROUTE, configWithToken(token, { data: { address } }))

View file

@ -20,8 +20,6 @@ export const getLocation = (callback: (pos: LatLngLiteral) => void) => {
export const watchLocation = (callback: (pos: LatLngLiteral) => void): number => { export const watchLocation = (callback: (pos: LatLngLiteral) => void): number => {
return window.navigator.geolocation.watchPosition( return window.navigator.geolocation.watchPosition(
position => { position => {
console.log('Watch?');
if (!position || !position.coords || !position.coords.latitude || !position.coords.longitude) if (!position || !position.coords || !position.coords.latitude || !position.coords.longitude)
return callback(null); return callback(null);