Merge pull request #38 from muerwre/develop

Develop
This commit is contained in:
muerwre 2021-08-06 11:39:07 +07:00 committed by GitHub
commit bc5892120f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 909 additions and 891 deletions

View file

@ -64,21 +64,4 @@ steps:
- cp -a $${ENV_PATH}/${DRONE_BRANCH}/. $${BUILD_PATH}/${DRONE_BRANCH} - cp -a $${ENV_PATH}/${DRONE_BRANCH}/. $${BUILD_PATH}/${DRONE_BRANCH}
- docker-compose build - docker-compose build
- docker-compose up -d - docker-compose up -d
# - name: telgram_notify
# image: appleboy/drone-telegram
# when:
# status:
# - success
# - failure
# settings:
# token:
# from_secret: telegram_token
# to:
# from_secret: telegram_chat_id
# format: markdown
# message: >
# {{#success build.status}}🤓{{else}}😨{{/success}}
# [{{repo.name}} / {{commit.branch}}]({{ build.link }})
# ```
# {{ commit.message }}
# ```

2
.env
View file

@ -1,4 +1,4 @@
REACT_APP_PUBLIC_PATH = https://localhost:3000/ REACT_APP_PUBLIC_PATH = https://localhost:3000/
REACT_APP_API_ADDR = https://backend.alpha-map.vault48.org/ REACT_APP_API_ADDR = https://backend.map.vault48.org
REACT_APP_OSRM_URL = https://vault48.org:5001/route/v1 REACT_APP_OSRM_URL = https://vault48.org:5001/route/v1
REACT_APP_OSRM_PROFILE = bike REACT_APP_OSRM_PROFILE = bike

View file

@ -32,7 +32,6 @@
"gpx-parser-builder": "^1.0.2", "gpx-parser-builder": "^1.0.2",
"leaflet": "1.6.0", "leaflet": "1.6.0",
"leaflet-editable": "^1.1.0", "leaflet-editable": "^1.1.0",
"leaflet-geometryutil": "^0.9.0",
"leaflet-routing-machine": "^3.2.12", "leaflet-routing-machine": "^3.2.12",
"leaflet.markercluster": "^1.4.1", "leaflet.markercluster": "^1.4.1",
"node-sass": "^5.0.0", "node-sass": "^5.0.0",

View file

@ -23,10 +23,15 @@ class StickerDesc extends React.PureComponent<Props, State> {
blockMouse = e => { blockMouse = e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!this.input) {
return
}
this.input.focus(); this.input.focus();
}; };
input: HTMLTextAreaElement; input: HTMLTextAreaElement | null = null;
render() { render() {
const { value: text } = this.props; const { value: text } = this.props;

View file

@ -64,9 +64,8 @@ export interface State {
class MapListDialogUnconnected extends PureComponent<Props, State> { class MapListDialogUnconnected extends PureComponent<Props, State> {
state = { state = {
menu_target: null, menu_target: '',
editor_target: null, editor_target: '',
is_editing: false, is_editing: false,
is_dropping: false, is_dropping: false,
}; };
@ -74,7 +73,7 @@ class MapListDialogUnconnected extends PureComponent<Props, State> {
startEditing = (editor_target: IRouteListItem['address']): void => startEditing = (editor_target: IRouteListItem['address']): void =>
this.setState({ this.setState({
editor_target, editor_target,
menu_target: null, menu_target: '',
is_editing: true, is_editing: true,
is_dropping: false, is_dropping: false,
}); });
@ -86,19 +85,19 @@ class MapListDialogUnconnected extends PureComponent<Props, State> {
hideMenu = (): void => hideMenu = (): void =>
this.setState({ this.setState({
menu_target: null, menu_target: '',
}); });
showDropCard = (editor_target: IRouteListItem['address']): void => showDropCard = (editor_target: IRouteListItem['address']): void =>
this.setState({ this.setState({
editor_target, editor_target,
menu_target: null, menu_target: '',
is_editing: false, is_editing: false,
is_dropping: true, is_dropping: true,
}); });
stopEditing = (): void => { stopEditing = (): void => {
this.setState({ editor_target: null }); this.setState({ editor_target: '' });
}; };
setTitle = ({ target: { value } }: { target: { value: string } }): void => { setTitle = ({ target: { value } }: { target: { value: string } }): void => {

View file

@ -27,7 +27,7 @@ const ProviderDialogUnconnected = ({ provider, mapSetProvider }: Props) => (
backgroundImage: `url(${replaceProviderUrl(item, { x: 5980, y: 2589, zoom: 13 })})`, backgroundImage: `url(${replaceProviderUrl(item, { x: 5980, y: 2589, zoom: 13 })})`,
}} }}
onMouseDown={() => mapSetProvider(item)} onMouseDown={() => mapSetProvider(item)}
key={PROVIDERS[item].name} key={PROVIDERS[item]?.name}
> >
{ {
provider === item && provider === item &&

View file

@ -42,7 +42,7 @@ export class TitleDialogUnconnected extends React.PureComponent<Props, State> {
this.setMaxHeight(); this.setMaxHeight();
} }
setMaxHeight = (): number => { setMaxHeight = () => {
if (!this.ref_sizer || !this.ref_title || !this.ref_text) return 0; if (!this.ref_sizer || !this.ref_title || !this.ref_text) return 0;
const { height: sizer_height } = this.ref_sizer.getBoundingClientRect(); const { height: sizer_height } = this.ref_sizer.getBoundingClientRect();

View file

@ -40,7 +40,7 @@ export const RouteRowView = ({
is_admin, is_admin,
is_published, is_published,
toggleStarred toggleStarred
}: Props): ReactElement<Props, null> => ( }: Props): ReactElement<Props> => (
<div className={classnames("route-row-view", { has_menu: tab === "my" })}> <div className={classnames("route-row-view", { has_menu: tab === "my" })}>
{(tab === TABS.PENDING || tab === TABS.STARRED) && is_admin && ( {(tab === TABS.PENDING || tab === TABS.STARRED) && is_admin && (
<div className="route-row-fav" onClick={toggleStarred.bind(null, address)}> <div className="route-row-fav" onClick={toggleStarred.bind(null, address)}>

View file

@ -1,4 +1,4 @@
import React, { PureComponent, useState, useCallback } from 'react'; import React, { PureComponent } from 'react';
import { MODES } from '~/constants/modes'; import { MODES } from '~/constants/modes';
import classnames from 'classnames'; import classnames from 'classnames';
@ -7,12 +7,12 @@ import { EditorDialog } from '~/components/panels/EditorDialog';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
editorChangeMode, editorChangeMode,
editorKeyPressed,
editorRedo,
editorStartEditing, editorStartEditing,
editorStopEditing, editorStopEditing,
editorTakeAShot, editorTakeAShot,
editorKeyPressed,
editorUndo, editorUndo,
editorRedo,
} from '~/redux/editor/actions'; } from '~/redux/editor/actions';
import { Tooltip } from '~/components/panels/Tooltip'; import { Tooltip } from '~/components/panels/Tooltip';
import { IState } from '~/redux/store'; import { IState } from '~/redux/store';
@ -47,6 +47,10 @@ type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {}
class EditorPanelUnconnected extends PureComponent<Props, void> { class EditorPanelUnconnected extends PureComponent<Props, void> {
componentDidMount() { componentDidMount() {
if (!this.panel) {
return;
}
window.addEventListener('keydown', this.onKeyPress as any); window.addEventListener('keydown', this.onKeyPress as any);
const obj = document.getElementById('control-dialog'); const obj = document.getElementById('control-dialog');
@ -57,7 +61,7 @@ class EditorPanelUnconnected extends PureComponent<Props, void> {
obj.style.width = String(width); obj.style.width = String(width);
} }
panel: HTMLElement = null; panel: HTMLDivElement | null = null;
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('keydown', this.onKeyPress as any); window.removeEventListener('keydown', this.onKeyPress as any);

View file

@ -31,6 +31,10 @@ class Component extends React.Component<Props, State> {
}; };
onImageLoaded = () => { onImageLoaded = () => {
if (!this.image) {
return
}
this.croppr = new Croppr(this.image, { this.croppr = new Croppr(this.image, {
onInitialize: this.onCropInit, onInitialize: this.onCropInit,
}); });
@ -57,12 +61,12 @@ class Component extends React.Component<Props, State> {
regionEl.append(this.logo); regionEl.append(this.logo);
}; };
croppr: Croppr; croppr?: Croppr;
logo: HTMLDivElement; logo: HTMLDivElement | null = null;
image: HTMLImageElement; image: HTMLImageElement | null = null;
logoImg: HTMLImageElement; logoImg: HTMLImageElement | null = null;
getImage = () => this.props.editorCropAShot(this.croppr.getValue()); getImage = () => this.props.editorCropAShot(this.croppr?.getValue());
render() { render() {
const { data } = this.props.editor.renderer; const { data } = this.props.editor.renderer;

View file

@ -7,7 +7,7 @@ interface Props {
max: number; max: number;
search: string; search: string;
distance: [number, number]; distance: [number, number];
onDistanceChange: (val: [number, number]) => void; onDistanceChange: (val: number[]) => void;
onSearchChange: ChangeEventHandler<HTMLInputElement>; onSearchChange: ChangeEventHandler<HTMLInputElement>;
} }

View file

@ -1,16 +1,14 @@
import { CLIENT } from '~/config/frontend';
export const API = { export const API = {
GET_GUEST: `${CLIENT.API_ADDR}/api/auth/`, GET_GUEST: `/api/auth/`,
CHECK_TOKEN: `${CLIENT.API_ADDR}/api/auth/`, CHECK_TOKEN: `/api/auth/`,
IFRAME_LOGIN_VK: `${CLIENT.API_ADDR}/api/auth/vk`, IFRAME_LOGIN_VK: `/api/auth/vk`,
GET_MAP: `${CLIENT.API_ADDR}/api/route/`, GET_MAP: `/api/route/`,
POST_MAP: `${CLIENT.API_ADDR}/api/route/`, POST_MAP: `/api/route/`,
GET_ROUTE_LIST: tab => `${CLIENT.API_ADDR}/api/route/list/${tab}`, GET_ROUTE_LIST: tab => `/api/route/list/${tab}`,
DROP_ROUTE: `${CLIENT.API_ADDR}/api/route/`, DROP_ROUTE: `/api/route/`,
MODIFY_ROUTE: `${CLIENT.API_ADDR}/api/route/`, MODIFY_ROUTE: `/api/route/`,
SET_STARRED: `${CLIENT.API_ADDR}/api/route/publish`, SET_STARRED: `/api/route/publish`,
}; };
export const API_RETRY_INTERVAL = 10; export const API_RETRY_INTERVAL = 10;

View file

@ -11,7 +11,7 @@ export interface IUser {
role: IRoles[keyof IRoles]; role: IRoles[keyof IRoles];
routes: {}; routes: {};
success: boolean; success: boolean;
id?: string; id: string;
uid: string; uid: string;
token?: string; token?: string;
photo: string; photo: string;
@ -31,9 +31,9 @@ export const DEFAULT_USER: IUser = {
role: ROLES.guest, role: ROLES.guest,
routes: {}, routes: {},
success: false, success: false,
id: null, id: '',
token: null, token: undefined,
photo: null, photo: '',
name: null, name: '',
uid: null, uid: '',
}; };

View file

@ -5,21 +5,6 @@ export interface IProvider {
} }
export type ITileMaps = Record<string, IProvider> export type ITileMaps = Record<string, IProvider>
// {
// WATERCOLOR: IProvider,
// DGIS: IProvider,
// DEFAULT: IProvider,
// DARQ: IProvider,
// BLANK: IProvider,
// HOT: IProvider,
// YSAT: IProvider,
// YMAP: IProvider,
// SAT: IProvider,
// ESAT: IProvider,
// CACHE_OSM: IProvider,
// CACHE_CARTO: IProvider,
// }
// Стили карт // Стили карт
const TILEMAPS: ITileMaps = { const TILEMAPS: ITileMaps = {
@ -53,7 +38,7 @@ const TILEMAPS: ITileMaps = {
const ENABLED: Array<keyof ITileMaps> = ['BLANK', 'DEFAULT', 'DGIS', 'HOT', 'ESAT']; const ENABLED: Array<keyof ITileMaps> = ['BLANK', 'DEFAULT', 'DGIS', 'HOT', 'ESAT'];
export const DEFAULT_PROVIDER: keyof ITileMaps = ENABLED[1]; export const DEFAULT_PROVIDER: keyof ITileMaps = ENABLED[1];
export const PROVIDERS: Partial<ITileMaps> = ENABLED.reduce((obj, provider) => ({ export const PROVIDERS: ITileMaps = ENABLED.reduce((obj, provider) => ({
...obj, ...obj,
[provider]: TILEMAPS[provider], [provider]: TILEMAPS[provider],
}), {}); }), {});

View file

@ -14,7 +14,7 @@ const mapDispatchToProps = {};
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {}; type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
const ArrowsUnconnected: FC<Props> = memo(({ route }) => { const ArrowsUnconnected: FC<Props> = memo(({ route }) => {
const [layer, setLayer] = useState(null); const [layer, setLayer] = useState<ArrowsLayer | null>(null);
useEffect(() => { useEffect(() => {
const item = new ArrowsLayer({}).addTo(MainMap); const item = new ArrowsLayer({}).addTo(MainMap);

View file

@ -1,9 +1,9 @@
import React, { FC, useState, useEffect, useCallback } from 'react'; import React, { FC, useCallback, useEffect } from 'react';
import { LatLngLiteral, marker, Marker, DivIcon } from 'leaflet'; import { DivIcon, LatLngLiteral, Marker } from 'leaflet';
import { MainMap } from '~/constants/map'; import { MainMap } from '~/constants/map';
interface IProps { interface IProps {
location: LatLngLiteral; location?: LatLngLiteral;
} }
const CurrentLocation: FC<IProps> = ({ location }) => { const CurrentLocation: FC<IProps> = ({ location }) => {

View file

@ -8,7 +8,7 @@ interface IProps {
} }
const GpxPolyline: FC<IProps> = ({ latlngs, color }) => { const GpxPolyline: FC<IProps> = ({ latlngs, color }) => {
const [layer, setLayer] = useState<Polyline>(null); const [layer, setLayer] = useState<Polyline | null>(null);
useEffect(() => { useEffect(() => {
const item = new Polyline([], { const item = new Polyline([], {

View file

@ -1,4 +1,4 @@
import React, { FC, useEffect, useState, memo } from 'react'; import React, { FC, memo, useEffect, useState } from 'react';
import { KmMarksLayer } from '~/utils/marks'; import { KmMarksLayer } from '~/utils/marks';
import { MainMap } from '~/constants/map'; import { MainMap } from '~/constants/map';
import { selectMap } from '~/redux/map/selectors'; import { selectMap } from '~/redux/map/selectors';
@ -14,14 +14,14 @@ const mapDispatchToProps = {};
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {}; type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
const KmMarksUnconnected: FC<Props> = memo(({ map: { route } }) => { const KmMarksUnconnected: FC<Props> = memo(({ map: { route } }) => {
const [layer, setLayer] = useState(null); const [layer, setLayer] = useState<KmMarksLayer | null>(null);
useEffect(() => { useEffect(() => {
const layer = new KmMarksLayer([]); const layer = new KmMarksLayer([]);
layer.addTo(MainMap); layer.addTo(MainMap);
setLayer(layer); setLayer(layer);
return () => MainMap.removeLayer(layer); return () => MainMap.removeLayer(layer);
}, [MainMap]); }, []);
useEffect(() => { useEffect(() => {
if (!layer) return; if (!layer) return;

View file

@ -93,7 +93,7 @@ const MapUnconnected: React.FC<IProps> = memo(
enabled && <GpxPolyline latlngs={latlngs} color={color} key={index} /> enabled && <GpxPolyline latlngs={latlngs} color={color} key={index} />
)} )}
</div>, </div>,
document.getElementById('canvas') document.getElementById('canvas')!
); );
} }
); );

View file

@ -1,8 +1,8 @@
import React, { FC, useEffect, memo, useState, useCallback } from 'react'; import React, { FC, memo, useCallback, useEffect, useState } from 'react';
import { InteractivePoly } from '~/utils/map/InteractivePoly'; import { InteractivePoly } from '~/utils/map/InteractivePoly';
import { isMobile } from '~/utils/window'; import { isMobile } from '~/utils/window';
import { LatLng } from 'leaflet'; import { LatLng } from 'leaflet';
import { selectEditorMode, selectEditorEditing, selectEditorDirection } from '~/redux/editor/selectors'; import { selectEditorDirection, selectEditorEditing, selectEditorMode } from '~/redux/editor/selectors';
import * as MAP_ACTIONS from '~/redux/map/actions'; import * as MAP_ACTIONS from '~/redux/map/actions';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectMapRoute } from '~/redux/map/selectors'; import { selectMapRoute } from '~/redux/map/selectors';
@ -28,7 +28,7 @@ type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {}
const RouteUnconnected: FC<Props> = memo( const RouteUnconnected: FC<Props> = memo(
({ route, editing, mode, drawing_direction, mapSetRoute, editorSetDistance, editorSetMarkersShown }) => { ({ route, editing, mode, drawing_direction, mapSetRoute, editorSetDistance, editorSetMarkersShown }) => {
const [layer, setLayer] = useState<InteractivePoly>(null); const [layer, setLayer] = useState<InteractivePoly | null>(null);
const onDistanceChange = useCallback(({ distance }) => editorSetDistance(distance), [ const onDistanceChange = useCallback(({ distance }) => editorSetDistance(distance), [
editorSetDistance, editorSetDistance,

View file

@ -1,16 +1,12 @@
import { FC, useEffect, useCallback, memo, useState } from 'react'; import { FC, memo, useCallback, useEffect, useState } from 'react';
import { OsrmRouter } from '~/utils/map/OsrmRouter'; import { OsrmRouter } from '~/utils/map/OsrmRouter';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectMapRoute } from '~/redux/map/selectors'; import { selectMapRoute } from '~/redux/map/selectors';
import { import { selectEditorDistance, selectEditorMode, selectEditorRouter } from '~/redux/editor/selectors';
selectEditorRouter,
selectEditorMode,
selectEditorDistance,
} from '~/redux/editor/selectors';
import { MainMap } from '~/constants/map'; import { MainMap } from '~/constants/map';
import * as EDITOR_ACTIONS from '~/redux/editor/actions'; import * as EDITOR_ACTIONS from '~/redux/editor/actions';
import { MODES } from '~/constants/modes'; import { MODES } from '~/constants/modes';
import { LatLngLiteral, marker, divIcon } from 'leaflet'; import { divIcon, LatLngLiteral, marker } from 'leaflet';
import classNames from 'classnames'; import classNames from 'classnames';
import { angleBetweenPoints } from '~/utils/geom'; import { angleBetweenPoints } from '~/utils/geom';
@ -30,7 +26,7 @@ type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {}
const RouterUnconnected: FC<Props> = memo( const RouterUnconnected: FC<Props> = memo(
({ route, mode, router: { waypoints }, editorSetRouter, distance }) => { ({ route, mode, router: { waypoints }, editorSetRouter, distance }) => {
const [dist, setDist] = useState(0); const [dist, setDist] = useState(0);
const [end, setEnd] = useState<LatLngLiteral>(null); const [end, setEnd] = useState<LatLngLiteral | null>(null);
const [direction, setDirection] = useState<boolean>(false); const [direction, setDirection] = useState<boolean>(false);
const updateWaypoints = useCallback( const updateWaypoints = useCallback(

View file

@ -20,8 +20,8 @@ interface IProps {
mapDropSticker: (index: number) => void; mapDropSticker: (index: number) => void;
} }
export const getLabelDirection = (angle: number): 'left' | 'right' => export const getLabelDirection = (angle?: number): 'left' | 'right' =>
angle % Math.PI >= -(Math.PI / 2) && angle % Math.PI <= Math.PI / 2 ? 'left' : 'right'; !!angle && angle % Math.PI >= -(Math.PI / 2) && angle % Math.PI <= Math.PI / 2 ? 'left' : 'right';
const getX = e => const getX = e =>
e.touches && e.touches.length > 0 e.touches && e.touches.length > 0
@ -36,50 +36,58 @@ const Sticker: React.FC<IProps> = ({
mapSetSticker, mapSetSticker,
mapDropSticker, mapDropSticker,
}) => { }) => {
const [text, setText] = useState(sticker.text); const [text, setText] = useState(sticker.text || '');
const [layer, setLayer] = React.useState<Marker>(null); const [layer, setLayer] = React.useState<Marker | null>(null);
const [dragging, setDragging] = React.useState(false); const [dragging, setDragging] = React.useState(false);
const wrapper = useRef(null); const wrapper = useRef<HTMLDivElement>(null);
let angle = useRef(sticker.angle); let angle = useRef(sticker.angle);
const element = React.useMemo(() => document.createElement('div'), []); const element = React.useMemo(() => document.createElement('div'), []);
const stickerArrow = React.useRef(null); const stickerArrow = React.useRef<HTMLDivElement>(null);
const stickerImage = React.useRef(null); const stickerImage = React.useRef<HTMLDivElement>(null);
const onChange = React.useCallback(state => mapSetSticker(index, state), [mapSetSticker, index]); const onChange = React.useCallback(state => mapSetSticker(index, state), [mapSetSticker, index]);
const onDelete = React.useCallback(state => mapDropSticker(index), [mapSetSticker, index]); const onDelete = React.useCallback(() => setTimeout(() => mapDropSticker(index), 0), [mapDropSticker, index]);
const updateAngle = useCallback( const updateAngle = useCallback(
ang => { ang => {
if (!stickerImage.current || !stickerArrow.current) return;
const x = Math.cos(ang + Math.PI) * 56 - 30; const x = Math.cos(ang + Math.PI) * 56 - 30;
const y = Math.sin(ang + Math.PI) * 56 - 30; const y = Math.sin(ang + Math.PI) * 56 - 30;
if (!stickerImage.current || !stickerArrow.current) {
return;
}
stickerImage.current.style.left = String(6 + x); stickerImage.current.style.left = String(6 + x);
stickerImage.current.style.top = String(6 + y); stickerImage.current.style.top = String(6 + y);
stickerArrow.current.style.transform = `rotate(${ang + Math.PI}rad)`; stickerArrow.current.style.transform = `rotate(${ang + Math.PI}rad)`;
}, },
[stickerArrow, stickerImage, angle] [stickerArrow, stickerImage],
); );
const onDragStart = React.useCallback(() => { const onDragStart = React.useCallback(() => {
if (!layer?.dragging) {
return;
}
layer.dragging.disable(); layer.dragging.disable();
MainMap.dragging.disable(); MainMap.dragging.disable();
MainMap.disableClicks(); MainMap.disableClicks();
setDragging(true); setDragging(true);
}, [setDragging, layer, MainMap]); }, [setDragging, layer]);
const onDragStop = React.useCallback( const onDragStop = React.useCallback(
event => { event => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
if (!layer) return; if (!layer?.dragging) {
return;
}
setDragging(false); setDragging(false);
onChange({ onChange({
@ -92,7 +100,7 @@ const Sticker: React.FC<IProps> = ({
setTimeout(MainMap.enableClicks, 100); setTimeout(MainMap.enableClicks, 100);
}, },
[setDragging, layer, MainMap, sticker, angle] [setDragging, layer, MainMap, sticker, angle],
); );
const onMoveStarted = React.useCallback(() => { const onMoveStarted = React.useCallback(() => {
@ -110,7 +118,7 @@ const Sticker: React.FC<IProps> = ({
MainMap.enableClicks(); MainMap.enableClicks();
}, },
[onChange, sticker] [onChange, sticker],
); );
const onDrag = React.useCallback( const onDrag = React.useCallback(
@ -122,7 +130,7 @@ const Sticker: React.FC<IProps> = ({
angle.current = parseFloat(Math.atan2(y - pageY, x - pageX).toFixed(2)); angle.current = parseFloat(Math.atan2(y - pageY, x - pageX).toFixed(2));
updateAngle(angle.current); updateAngle(angle.current);
}, },
[element, updateAngle, angle] [element, updateAngle, angle],
); );
const onTextChange = React.useCallback(text => setText(text), [sticker, onChange]); const onTextChange = React.useCallback(text => setText(text), [sticker, onChange]);
@ -134,7 +142,9 @@ const Sticker: React.FC<IProps> = ({
}); });
}, [text, onChange, sticker]); }, [text, onChange, sticker]);
const direction = React.useMemo(() => getLabelDirection(sticker.angle), [sticker.angle]); const direction = React.useMemo(() => {
getLabelDirection(sticker?.angle);
}, [sticker.angle]);
useEffect(() => { useEffect(() => {
updateAngle(sticker.angle); updateAngle(sticker.angle);
@ -148,15 +158,16 @@ const Sticker: React.FC<IProps> = ({
useEffect(() => { useEffect(() => {
if (!layer) return; if (!layer) return;
setText(sticker.text);
setText(sticker.text || '');
}, [layer, sticker.text]); }, [layer, sticker.text]);
useEffect(() => { useEffect(() => {
if (!wrapper || !wrapper.current) return; if (!wrapper || !wrapper.current) return;
const scale = getAdaptiveScale(zoom) // adaptive zoom :-) const scale = getAdaptiveScale(zoom); // adaptive zoom :-)
wrapper.current.style.transform = `scale(${scale}) perspective(1px)` wrapper.current.style.transform = `scale(${scale}) perspective(1px)`;
}, [zoom, wrapper]); }, [zoom, wrapper]);
// Attaches onMoveFinished event to item // Attaches onMoveFinished event to item
@ -235,7 +246,7 @@ const Sticker: React.FC<IProps> = ({
<div className="sticker-delete" onMouseDown={onDelete} onTouchStart={onDelete} /> <div className="sticker-delete" onMouseDown={onDelete} onTouchStart={onDelete} />
</div> </div>
</div>, </div>,
element element,
); );
}; };

View file

@ -14,7 +14,7 @@ interface IProps {
} }
const Stickers: FC<IProps> = memo(({ stickers, is_editing, mapSetSticker, mapDropSticker }) => { const Stickers: FC<IProps> = memo(({ stickers, is_editing, mapSetSticker, mapDropSticker }) => {
const [layer, setLayer] = useState<FeatureGroup>(null); const [layer, setLayer] = useState<FeatureGroup | null>(null);
const [zoom, setZoom] = useState(MainMap.getZoom()); const [zoom, setZoom] = useState(MainMap.getZoom());
const onZoomChange = useCallback( const onZoomChange = useCallback(

View file

@ -10,7 +10,7 @@ type IProps = React.HTMLAttributes<HTMLDivElement> & {
}; };
const TileLayer: React.FC<IProps> = React.memo(({ children, provider, map }) => { const TileLayer: React.FC<IProps> = React.memo(({ children, provider, map }) => {
const [layer, setLayer] = React.useState<TileLayerInterface>(null); const [layer, setLayer] = React.useState<TileLayerInterface | undefined>(undefined);
React.useEffect(() => { React.useEffect(() => {
if (!map) return; if (!map) return;
@ -34,7 +34,11 @@ const TileLayer: React.FC<IProps> = React.memo(({ children, provider, map }) =>
layer.setUrl(url); layer.setUrl(url);
}, [layer, provider]); }, [layer, provider]);
return <TileContext.Provider value={layer}>{children}</TileContext.Provider>; return (
<TileContext.Provider value={layer}>
{children}
</TileContext.Provider>
);
}); });
export { TileLayer }; export { TileLayer };

View file

@ -41,7 +41,7 @@ export interface IEditorState {
distance: number; distance: number;
estimated: number; estimated: number;
speed: number; speed: number;
activeSticker: { set?: string; sticker?: string }; activeSticker: { set: string; sticker: string };
is_empty: boolean; is_empty: boolean;
is_published: boolean; is_published: boolean;
is_routing: boolean; is_routing: boolean;
@ -134,7 +134,7 @@ export const EDITOR_INITIAL_STATE = {
}, },
save: { save: {
error: null, error: '',
finished: false, finished: false,
overwriting: false, overwriting: false,
processing: false, processing: false,

View file

@ -69,8 +69,14 @@ import uuid from 'uuid';
import { getRandomColor, getAdaptiveScale } from '~/utils/dom'; import { getRandomColor, getAdaptiveScale } from '~/utils/dom';
const hideLoader = () => { const hideLoader = () => {
document.getElementById('loader').style.opacity = String(0); const el = document.getElementById('loader');
document.getElementById('loader').style.pointerEvents = 'none';
if (!el) {
return true;
}
el.style.opacity = String(0);
el.style.pointerEvents = 'none';
return true; return true;
}; };
@ -125,6 +131,10 @@ function* getRenderData() {
canvas.height = window.innerHeight; canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) {
return
}
const geometry = getTilePlacement(); const geometry = getTilePlacement();
const points = getPolyPlacement(route); const points = getPolyPlacement(route);
const sticker_points = getStickersPlacement(stickers); const sticker_points = getStickersPlacement(stickers);
@ -184,6 +194,11 @@ function* getCropData({ x, y, width, height }) {
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) {
return
}
const image = yield imageFetcher(data); const image = yield imageFetcher(data);
ctx.drawImage(image, -x, -y); ctx.drawImage(image, -x, -y);
@ -281,7 +296,7 @@ function* mapClick({ latlng }: ReturnType<typeof mapClicked>) {
function* routerSubmit() { function* routerSubmit() {
const route: ReturnType<typeof selectMapRoute> = yield select(selectMapRoute); const route: ReturnType<typeof selectMapRoute> = yield select(selectMapRoute);
const latlngs: LatLng[] = path(['_routes', 0, 'coordinates'], OsrmRouter); const latlngs: LatLng[] = path(['_routes', 0, 'coordinates'], OsrmRouter) || [];
const coordinates = simplify(latlngs); const coordinates = simplify(latlngs);

View file

@ -28,7 +28,7 @@ export const MAP_INITIAL_STATE: IMapReducer = {
address: '', address: '',
address_origin: '', address_origin: '',
description: '', description: '',
owner: { id: null }, owner: { id: '' },
is_public: false, is_public: false,
zoom: 13, zoom: 13,
} }

View file

@ -1,40 +1,30 @@
import { import { call, delay, put, race, select, take, TakeEffect, takeEvery, takeLatest } from 'redux-saga/effects';
takeEvery,
select,
put,
call,
TakeEffect,
race,
take,
takeLatest,
delay,
} from 'redux-saga/effects';
import { MAP_ACTIONS } from './constants'; import { MAP_ACTIONS } from './constants';
import { import {
mapClicked,
mapAddSticker, mapAddSticker,
mapSetProvider, mapClicked,
mapSet, mapSet,
mapSetTitle,
mapSetAddressOrigin, mapSetAddressOrigin,
mapSetProvider,
mapSetRoute, mapSetRoute,
mapSetStickers, mapSetStickers,
mapSetTitle,
} from './actions'; } from './actions';
import { selectUser, selectUserUser } from '~/redux/user/selectors'; import { selectUser } from '~/redux/user/selectors';
import { MODES } from '~/constants/modes'; import { MODES } from '~/constants/modes';
import { import {
editorCaptureHistory,
editorChangeMode, editorChangeMode,
editorClearAll,
editorSendSaveRequest,
editorSetActiveSticker,
editorSetChanged, editorSetChanged,
editorSetEditing, editorSetEditing,
editorSetReady,
editorSetActiveSticker,
editorSendSaveRequest,
editorSetSave,
editorClearAll,
editorSetHistory, editorSetHistory,
editorCaptureHistory, editorSetReady,
editorSetSave,
} from '~/redux/editor/actions'; } from '~/redux/editor/actions';
import { pushLoaderState, getUrlData, pushPath } from '~/utils/history'; import { getUrlData, pushLoaderState, pushPath } from '~/utils/history';
import { getStoredMap, postMap } from '~/utils/api'; 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';
@ -70,8 +60,11 @@ export function* replaceAddressIfItsBusy(destination, original) {
} }
export function* loadMapSaga(path) { export function* loadMapSaga(path) {
try {
const { const {
data: { route, error, random_url }, data: {
route, error, random_url,
},
}: Unwrap<typeof getStoredMap> = yield call(getStoredMap, { name: path }); }: Unwrap<typeof getStoredMap> = yield call(getStoredMap, { name: path });
if (route && !error) { if (route && !error) {
@ -85,7 +78,7 @@ export function* loadMapSaga(path) {
description: route.description, description: route.description,
is_public: route.is_public, is_public: route.is_public,
logo: route.logo, logo: route.logo,
}) }),
); );
yield put(editorSetHistory({ records: [{ route: route.route, stickers: route.stickers }] })); yield put(editorSetHistory({ records: [{ route: route.route, stickers: route.stickers }] }));
@ -93,6 +86,10 @@ export function* loadMapSaga(path) {
} }
return null; return null;
} catch (e) {
console.log(e);
yield call(startEmptyEditorSaga);
}
} }
export function* startEmptyEditorSaga() { export function* startEmptyEditorSaga() {
@ -142,10 +139,10 @@ export function* mapInitSaga() {
yield put(mapSetProvider(provider)); yield put(mapSetProvider(provider));
if (hash && /^#map/.test(hash)) { if (hash && /^#map/.test(hash)) {
const [, newUrl] = hash.match(/^#map[:/?!](.*)$/); const matches = hash.match(/^#map[:/?!](.*)$/);
if (newUrl) { if (matches && matches[1]) {
yield pushPath(`/${newUrl}`); yield pushPath(`/${matches[1]}`);
yield call(setReadySaga); yield call(setReadySaga);
return; return;
} }
@ -161,7 +158,7 @@ function* setActiveStickerSaga() {
yield put(editorChangeMode(MODES.STICKERS)); yield put(editorChangeMode(MODES.STICKERS));
} }
function* setTitleSaga({ title }: ReturnType<typeof mapSetTitle>) { function setTitleSaga({ title }: ReturnType<typeof mapSetTitle>) {
if (title) { if (title) {
document.title = `${title} | Редактор маршрутов`; document.title = `${title} | Редактор маршрутов`;
} }
@ -216,7 +213,7 @@ function* clearSaga({ type }) {
const { mode, activeSticker }: ReturnType<typeof selectEditor> = yield select(selectEditor); const { mode, activeSticker }: ReturnType<typeof selectEditor> = yield select(selectEditor);
if (activeSticker && activeSticker.set && activeSticker.sticker) { if (activeSticker && activeSticker.set && activeSticker.sticker) {
yield put(editorSetActiveSticker(null)); yield put(editorSetActiveSticker({ set: '', sticker: '' }));
} }
if (mode !== MODES.NONE) { if (mode !== MODES.NONE) {
@ -231,19 +228,19 @@ function* sendSaveRequestSaga({
is_public, is_public,
description, description,
}: ReturnType<typeof editorSendSaveRequest>) { }: ReturnType<typeof editorSendSaveRequest>) {
try {
const { route, stickers, provider }: ReturnType<typeof selectMap> = yield select(selectMap); const { route, stickers, provider }: ReturnType<typeof selectMap> = yield select(selectMap);
if (!route.length && !stickers.length) { if (!route.length && !stickers.length) {
return yield put( return yield put(
editorSetSave({ error: TIPS.SAVE_EMPTY, loading: false, overwriting: false, finished: false }) editorSetSave({ error: TIPS.SAVE_EMPTY, loading: false, overwriting: false, finished: false }),
); );
} }
const { logo }: ReturnType<typeof selectMap> = yield select(selectMap); const { logo }: ReturnType<typeof selectMap> = yield select(selectMap);
const { distance }: ReturnType<typeof selectEditor> = yield select(selectEditor); const { distance }: ReturnType<typeof selectEditor> = yield select(selectEditor);
const { token }: ReturnType<typeof selectUserUser> = yield select(selectUserUser);
yield put(editorSetSave({ loading: true, overwriting: false, finished: false, error: null })); yield put(editorSetSave({ loading: true, overwriting: false, finished: false, error: '' }));
const { const {
result, result,
@ -255,7 +252,6 @@ function* sendSaveRequestSaga({
cancel: TakeEffect; cancel: TakeEffect;
} = yield race({ } = yield race({
result: postMap({ result: postMap({
token,
route, route,
stickers, stickers,
title, title,
@ -285,7 +281,7 @@ function* sendSaveRequestSaga({
loading: false, loading: false,
overwriting: false, overwriting: false,
finished: false, finished: false,
}) }),
); );
if (timeout || !result || !result.data.route || !result.data.route.address) if (timeout || !result || !result.data.route || !result.data.route.address)
@ -295,7 +291,7 @@ function* sendSaveRequestSaga({
loading: false, loading: false,
overwriting: false, overwriting: false,
finished: false, finished: false,
}) }),
); );
yield put( yield put(
@ -304,7 +300,7 @@ function* sendSaveRequestSaga({
title: result.data.route.title, title: result.data.route.title,
is_public: result.data.route.is_public, is_public: result.data.route.is_public,
description: result.data.route.description, description: result.data.route.description,
}) }),
); );
yield put(editorSetReady(false)); yield put(editorSetReady(false));
@ -317,8 +313,11 @@ function* sendSaveRequestSaga({
loading: false, loading: false,
overwriting: false, overwriting: false,
finished: true, finished: true,
}) }),
); );
} catch (e) {
console.log(e);
}
} }
function* setChanged() { function* setChanged() {
@ -328,14 +327,10 @@ function* setChanged() {
yield put(editorSetChanged(true)); yield put(editorSetChanged(true));
} }
function* onZoomChange() {
}
export function* mapSaga() { export function* mapSaga() {
yield takeEvery( yield takeEvery(
[MAP_ACTIONS.SET_ROUTE, MAP_ACTIONS.SET_STICKER, MAP_ACTIONS.SET_STICKERS], [MAP_ACTIONS.SET_ROUTE, MAP_ACTIONS.SET_STICKER, MAP_ACTIONS.SET_STICKERS, MAP_ACTIONS.ADD_STICKER],
setChanged setChanged,
); );
yield takeEvery(EDITOR_ACTIONS.START_EDITING, startEditingSaga); yield takeEvery(EDITOR_ACTIONS.START_EDITING, startEditingSaga);
@ -351,6 +346,6 @@ export function* mapSaga() {
EDITOR_ACTIONS.CLEAR_ALL, EDITOR_ACTIONS.CLEAR_ALL,
EDITOR_ACTIONS.CLEAR_CANCEL, EDITOR_ACTIONS.CLEAR_CANCEL,
], ],
clearSaga clearSaga,
); );
} }

View file

@ -1,6 +1,6 @@
import { createStore, applyMiddleware, combineReducers, compose, Store } from 'redux'; import { applyMiddleware, combineReducers, compose, createStore, Store } from 'redux';
import { persistStore, persistReducer } from 'redux-persist'; import { persistReducer, persistStore } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; import storage from 'redux-persist/lib/storage';
import createSagaMiddleware from 'redux-saga'; import createSagaMiddleware from 'redux-saga';
@ -8,19 +8,28 @@ import { createBrowserHistory } from 'history';
import { editorLocationChanged } from '~/redux/editor/actions'; import { editorLocationChanged } from '~/redux/editor/actions';
import { PersistConfig, Persistor } from 'redux-persist/es/types'; import { PersistConfig, Persistor } from 'redux-persist/es/types';
import { userReducer, IRootReducer } from '~/redux/user'; import { IRootReducer, userReducer } from '~/redux/user';
import { userSaga } from '~/redux/user/sagas'; import { userSaga } from '~/redux/user/sagas';
import { editor, IEditorState } from '~/redux/editor'; import { editor, IEditorState } from '~/redux/editor';
import { editorSaga } from '~/redux/editor/sagas'; import { editorSaga } from '~/redux/editor/sagas';
import { map, IMapReducer } from '~/redux/map'; import { IMapReducer, map } from '~/redux/map';
import { mapSaga } from '~/redux/map/sagas'; import { mapSaga } from '~/redux/map/sagas';
import { watchLocation, getLocation } from '~/utils/window'; import { watchLocation } from '~/utils/window';
import { LatLngLiteral } from 'leaflet'; import { LatLngLiteral } from 'leaflet';
import { setUserLocation } from './user/actions'; import { setUserLocation, userLogout } from './user/actions';
import { MainMap } from '~/constants/map'; import { MainMap } from '~/constants/map';
import { mapZoomChange } from './map/actions'; import { mapZoomChange } from './map/actions';
import { assocPath } from 'ramda';
import { AxiosError } from 'axios';
import { api } from '~/utils/api/instance';
const mapPersistConfig: PersistConfig = {
key: 'map',
whitelist: ['logo', 'provider'],
storage,
};
const userPersistConfig: PersistConfig = { const userPersistConfig: PersistConfig = {
key: 'user', key: 'user',
@ -52,7 +61,7 @@ export const store = createStore(
combineReducers({ combineReducers({
user: persistReducer(userPersistConfig, userReducer), user: persistReducer(userPersistConfig, userReducer),
editor: persistReducer(editorPersistConfig, editor), editor: persistReducer(editorPersistConfig, editor),
map, map: persistReducer(mapPersistConfig, map),
}), }),
composeEnhancers(applyMiddleware(sagaMiddleware)) composeEnhancers(applyMiddleware(sagaMiddleware))
); );
@ -64,6 +73,28 @@ export function configureStore(): { store: Store<any>; persistor: Persistor } {
const persistor = persistStore(store); const persistor = persistStore(store);
// Pass token to axios
api.interceptors.request.use(options => {
const token = store.getState().user.user.token;
if (!token) {
return options;
}
return assocPath(['headers', 'authorization'], token, options);
});
// Logout on 401
api.interceptors.response.use(undefined, (error: AxiosError<{ error: string }>) => {
if (error.response?.status === 401) {
store.dispatch(userLogout());
}
error.message = error?.response?.data?.error || error?.response?.statusText || error.message;
throw error;
});
return { store, persistor }; return { store, persistor };
} }
@ -74,5 +105,5 @@ history.listen((location, action) => {
store.dispatch(editorLocationChanged(location.pathname)); store.dispatch(editorLocationChanged(location.pathname));
}); });
watchLocation((location: LatLngLiteral) => store.dispatch(setUserLocation(location))); watchLocation((location: LatLngLiteral | undefined) => store.dispatch(setUserLocation(location)));
MainMap.on('zoomend', event => store.dispatch(mapZoomChange(event.target._zoom))) MainMap.on('zoomend', event => store.dispatch(mapZoomChange(event.target._zoom)))

View file

@ -15,7 +15,7 @@ export interface IRouteListItem {
export interface IRootReducer { export interface IRootReducer {
// ready: boolean, // ready: boolean,
user: IUser; user: IUser;
location: LatLngLiteral; location?: LatLngLiteral;
routes: { routes: {
limit: 0; limit: 0;
loading: boolean; loading: boolean;
@ -38,7 +38,7 @@ export type IRootState = Readonly<IRootReducer>;
export const INITIAL_STATE: IRootReducer = { export const INITIAL_STATE: IRootReducer = {
user: { ...DEFAULT_USER }, user: { ...DEFAULT_USER },
location: null, location: undefined,
routes: { routes: {
limit: 0, limit: 0,
loading: false, // <-- maybe delete this loading: false, // <-- maybe delete this

View file

@ -1,5 +1,5 @@
import { REHYDRATE, RehydrateAction } from 'redux-persist'; import { REHYDRATE, RehydrateAction } from 'redux-persist';
import { takeLatest, select, call, put, takeEvery, delay } from 'redux-saga/effects'; import { call, delay, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { import {
checkIframeToken, checkIframeToken,
checkUserToken, checkUserToken,
@ -9,15 +9,16 @@ import {
modifyRoute, modifyRoute,
sendRouteStarred, sendRouteStarred,
} from '~/utils/api'; } from '~/utils/api';
import * as ActionCreators from '~/redux/user/actions';
import { import {
searchSetTab,
setUser,
mapsSetShift, mapsSetShift,
searchChangeDistance, searchChangeDistance,
searchPutRoutes, searchPutRoutes,
searchSetLoading, searchSetLoading,
searchSetTab,
searchSetTitle, searchSetTitle,
setRouteStarred, setRouteStarred,
setUser,
userLogin, userLogin,
} from '~/redux/user/actions'; } from '~/redux/user/actions';
@ -26,8 +27,6 @@ import { USER_ACTIONS } from '~/redux/user/constants';
import { DEFAULT_USER } from '~/constants/auth'; import { DEFAULT_USER } from '~/constants/auth';
import { DIALOGS, TABS } from '~/constants/dialogs'; import { DIALOGS, TABS } from '~/constants/dialogs';
import * as ActionCreators from '~/redux/user/actions';
import { Unwrap } from '~/utils/middleware'; import { Unwrap } from '~/utils/middleware';
import { selectUser, selectUserUser } from './selectors'; import { selectUser, selectUserUser } from './selectors';
import { mapInitSaga } from '~/redux/map/sagas'; import { mapInitSaga } from '~/redux/map/sagas';
@ -35,6 +34,7 @@ import { editorSetDialog, editorSetDialogActive } from '../editor/actions';
import { selectEditor } from '../editor/selectors'; import { selectEditor } from '../editor/selectors';
function* generateGuestSaga() { function* generateGuestSaga() {
try {
const { const {
data: { user, random_url }, data: { user, random_url },
}: Unwrap<typeof getGuestToken> = yield call(getGuestToken); }: Unwrap<typeof getGuestToken> = yield call(getGuestToken);
@ -42,9 +42,13 @@ function* generateGuestSaga() {
yield put(setUser({ ...user, random_url })); yield put(setUser({ ...user, random_url }));
return { ...user, random_url }; return { ...user, random_url };
} catch(e) {
console.log(e)
}
} }
function* authCheckSaga({ key }: RehydrateAction) { function* authCheckSaga({ key }: RehydrateAction) {
try {
if (key !== 'user') return; if (key !== 'user') return;
pushLoaderState(70); pushLoaderState(70);
@ -93,6 +97,9 @@ function* authCheckSaga({ key }: RehydrateAction) {
pushLoaderState(80); pushLoaderState(80);
return yield call(mapInitSaga); return yield call(mapInitSaga);
} catch (e) {
console.log(e);
}
} }
function* gotVkUserSaga({ user: u }: ReturnType<typeof ActionCreators.gotVkUser>) { function* gotVkUserSaga({ user: u }: ReturnType<typeof ActionCreators.gotVkUser>) {
@ -105,8 +112,7 @@ function* gotVkUserSaga({ user: u }: ReturnType<typeof ActionCreators.gotVkUser>
} }
function* searchGetRoutes() { function* searchGetRoutes() {
const { token }: ReturnType<typeof selectUserUser> = yield select(selectUserUser); try {
const { const {
routes: { routes: {
step, step,
@ -116,7 +122,6 @@ function* searchGetRoutes() {
}: ReturnType<typeof selectUser> = yield select(selectUser); }: ReturnType<typeof selectUser> = yield select(selectUser);
const result: Unwrap<typeof getRouteList> = yield getRouteList({ const result: Unwrap<typeof getRouteList> = yield getRouteList({
token,
search: title, search: title,
min: distance[0], min: distance[0],
max: distance[1], max: distance[1],
@ -126,9 +131,13 @@ function* searchGetRoutes() {
}); });
return result; return result;
} catch (e) {
console.log(e);
}
} }
export function* searchSetSagaWorker() { export function* searchSetSagaWorker() {
try {
const { const {
routes: { filter }, routes: { filter },
}: ReturnType<typeof selectUser> = yield select(selectUser); }: ReturnType<typeof selectUser> = yield select(selectUser);
@ -152,11 +161,14 @@ export function* searchSetSagaWorker() {
searchChangeDistance([ searchChangeDistance([
filter.min > min && filter.distance[0] <= filter.min ? min : filter.distance[0], filter.min > min && filter.distance[0] <= filter.min ? min : filter.distance[0],
filter.max < max && filter.distance[1] >= filter.max ? max : filter.distance[1], filter.max < max && filter.distance[1] >= filter.max ? max : filter.distance[1],
]) ]),
); );
} }
return yield put(searchSetLoading(false)); return yield put(searchSetLoading(false));
} catch (e) {
console.log(e);
}
} }
function* searchSetSaga() { function* searchSetSaga() {
@ -167,6 +179,7 @@ function* searchSetSaga() {
} }
function* openMapDialogSaga({ tab }: ReturnType<typeof ActionCreators.openMapDialog>) { function* openMapDialogSaga({ tab }: ReturnType<typeof ActionCreators.openMapDialog>) {
try {
const { const {
routes: { routes: {
filter: { tab: current }, filter: { tab: current },
@ -187,6 +200,9 @@ function* openMapDialogSaga({ tab }: ReturnType<typeof ActionCreators.openMapDia
yield put(editorSetDialogActive(true)); yield put(editorSetDialogActive(true));
return tab; return tab;
} catch (e) {
console.log(e);
}
} }
function* searchSetTabSaga() { function* searchSetTabSaga() {
@ -210,6 +226,7 @@ function* setUserSaga() {
} }
function* mapsLoadMoreSaga() { function* mapsLoadMoreSaga() {
try {
const { const {
routes: { limit, list, shift, step, loading, filter }, routes: { limit, list, shift, step, loading, filter },
}: ReturnType<typeof selectUser> = yield select(selectUser); }: ReturnType<typeof selectUser> = yield select(selectUser);
@ -237,7 +254,7 @@ function* mapsLoadMoreSaga() {
searchChangeDistance([ searchChangeDistance([
filter.min > min && filter.distance[0] <= filter.min ? min : filter.distance[0], filter.min > min && filter.distance[0] <= filter.min ? min : filter.distance[0],
filter.max < max && filter.distance[1] >= filter.max ? max : filter.distance[1], filter.max < max && filter.distance[1] >= filter.max ? max : filter.distance[1],
]) ]),
); );
} }
@ -249,13 +266,16 @@ function* mapsLoadMoreSaga() {
shift: resp_shift, shift: resp_shift,
step: resp_step, step: resp_step,
list: [...list, ...routes], list: [...list, ...routes],
}) }),
); );
yield put(searchSetLoading(false)); yield put(searchSetLoading(false));
} catch (e) {
console.log(e);
}
} }
function* dropRouteSaga({ address }: ReturnType<typeof ActionCreators.dropRoute>) { function* dropRouteSaga({ address }: ReturnType<typeof ActionCreators.dropRoute>) {
const { token }: ReturnType<typeof selectUserUser> = yield select(selectUserUser); try {
const { const {
routes: { routes: {
list, list,
@ -277,11 +297,14 @@ function* dropRouteSaga({ address }: ReturnType<typeof ActionCreators.dropRoute>
step, step,
shift: shift > 0 ? shift - 1 : 0, shift: shift > 0 ? shift - 1 : 0,
limit: limit > 0 ? limit - 1 : limit, limit: limit > 0 ? limit - 1 : limit,
}) }),
); );
} }
return yield call(dropRoute, { address, token }); return yield call(dropRoute, { address });
} catch (e) {
console.log(e);
}
} }
function* modifyRouteSaga({ function* modifyRouteSaga({
@ -289,7 +312,7 @@ function* modifyRouteSaga({
title, title,
is_public, is_public,
}: ReturnType<typeof ActionCreators.modifyRoute>) { }: ReturnType<typeof ActionCreators.modifyRoute>) {
const { token }: ReturnType<typeof selectUserUser> = yield select(selectUserUser); try {
const { const {
routes: { routes: {
list, list,
@ -311,31 +334,37 @@ function* modifyRouteSaga({
step, step,
shift: shift > 0 ? shift - 1 : 0, shift: shift > 0 ? shift - 1 : 0,
limit: limit > 0 ? limit - 1 : limit, limit: limit > 0 ? limit - 1 : limit,
}) }),
); );
} }
return yield call(modifyRoute, { address, token, title, is_public }); return yield call(modifyRoute, { address, title, is_public });
} catch (e) {
console.log(e);
}
} }
function* toggleRouteStarredSaga({ function* toggleRouteStarredSaga({
address, address,
}: ReturnType<typeof ActionCreators.toggleRouteStarred>) { }: ReturnType<typeof ActionCreators.toggleRouteStarred>) {
const { token }: ReturnType<typeof selectUserUser> = yield select(selectUserUser); try {
const { const {
routes: { list }, routes: { list },
}: ReturnType<typeof selectUser> = yield select(selectUser); }: ReturnType<typeof selectUser> = yield select(selectUser);
const route = list.find(el => el.address === address); const route = list.find(el => el.address === address);
yield put(setRouteStarred(address, !route.is_published)); yield put(setRouteStarred(address, !route?.is_published));
const result = yield sendRouteStarred({ const result = yield sendRouteStarred({
token,
address, address,
is_published: !route.is_published, is_published: !route?.is_published,
}); });
if (!result) return yield put(setRouteStarred(address, route.is_published)); if (!result) return yield put(setRouteStarred(address, !!route?.is_published));
} catch (e) {
console.log(e);
}
} }
export function* updateUserRoutes() { export function* updateUserRoutes() {
@ -350,7 +379,7 @@ export function* userSaga() {
yield takeLatest( yield takeLatest(
[USER_ACTIONS.SEARCH_SET_TITLE, USER_ACTIONS.SEARCH_SET_DISTANCE], [USER_ACTIONS.SEARCH_SET_TITLE, USER_ACTIONS.SEARCH_SET_DISTANCE],
searchSetSaga searchSetSaga,
); );
yield takeLatest(USER_ACTIONS.OPEN_MAP_DIALOG, openMapDialogSaga); yield takeLatest(USER_ACTIONS.OPEN_MAP_DIALOG, openMapDialogSaga);

View file

@ -32,4 +32,4 @@ $tooltip_background: #123740;
$loading_shade: darken($blue_secondary, 20%); $loading_shade: darken($blue_secondary, 20%);
$cluster_small: #0069a7; $cluster_small: #0069a7;
$title_dialog_color: fade(#111111, 85%); $title_dialog_color: darken(#111111, 85%);

View file

@ -178,7 +178,7 @@
left: 0; left: 0;
width: 100%; width: 100%;
z-index: 10; z-index: 10;
background: linear-gradient(fade($loading_shade, 0%), $loading_shade 70%); background: linear-gradient(darken($loading_shade, 0%), $loading_shade 70%);
height: 100px; height: 100px;
pointer-events: none; pointer-events: none;
transition: opacity 100ms; transition: opacity 100ms;
@ -240,7 +240,7 @@
&.has_edit { &.has_edit {
//transform: translateY(-2px); //transform: translateY(-2px);
.route-row { .route-row {
background: fade($green_secondary, 30%); background: darken($green_secondary, 30%);
} }
} }
@ -280,11 +280,11 @@
} }
.route-row-edit { .route-row-edit {
background: fade($green_secondary, 30%); background: darken($green_secondary, 30%);
} }
.route-row-drop { .route-row-drop {
background: fade($red_secondary, 20%); background: darken($red_secondary, 20%);
.route-row { .route-row {
align-items: center; align-items: center;
@ -341,13 +341,13 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
fill: fade(white, 30%); fill: darken(white, 30%);
background: fade(white, 8%); background: transparentize(white, 0.9);
cursor: pointer; cursor: pointer;
transition: background 250ms, transform 500ms; transition: background 250ms, transform 500ms;
&:hover { &:hover {
background: fade(white, 10%); background: transparentize(white, 0.95);
} }
} }
@ -361,7 +361,7 @@
overflow: hidden; overflow: hidden;
transition: all 500ms; transition: all 500ms;
display: flex; display: flex;
fill: fade(white, 30%); fill: darken(white, 30%);
div { div {
width: 60px; width: 60px;
@ -371,16 +371,16 @@
align-items: center; align-items: center;
&:first-child { &:first-child {
box-shadow: fade(black, 30%) 1px 0; box-shadow: darken(black, 30%) 1px 0;
} }
&:hover { &:hover {
background: fade($red_secondary, 30%); background: darken($red_secondary, 30%);
} }
&.modify-button { &.modify-button {
&:hover { &:hover {
background: fade($green_secondary, 30%); background: darken($green_secondary, 30%);
} }
} }
} }
@ -399,7 +399,7 @@
.route-row-corner { .route-row-corner {
svg { svg {
fill: fade(white, 50%); fill: darken(white, 50%);
margin-right: 2px; margin-right: 2px;
flex-shrink: 0; flex-shrink: 0;
} }

View file

@ -12,7 +12,28 @@
} }
.leaflet-control-zoom { .leaflet-control-zoom {
display: none; width: 32px;
opacity: 0.5;
transition: opacity 0.1s;
&:hover {
opacity: 1;
}
}
a.leaflet-control-zoom-in, a.leaflet-control-zoom-out {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
width: 32px;
height: 32px;
display: flex;
opacity: 1;
}
} }
.leaflet-touch .leaflet-bar a { .leaflet-touch .leaflet-bar a {
@ -308,7 +329,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: white; color: white;
box-shadow: fade($cluster_small, 70%) 0 0 0 5px; box-shadow: darken($cluster_small, 70%) 0 0 0 5px;
font-weight: bold; font-weight: bold;
font-size: 13px; font-size: 13px;
transform: translate(-12px, -12px); transform: translate(-12px, -12px);
@ -317,7 +338,7 @@
outline: none; outline: none;
&:hover { &:hover {
box-shadow: fade($cluster_small, 70%) 0 0 0 7px; box-shadow: darken($cluster_small, 70%) 0 0 0 7px;
} }
span { span {

View file

@ -729,7 +729,7 @@
margin-bottom: 10px; margin-bottom: 10px;
padding: 10px; padding: 10px;
background: $title_dialog_color; background: $title_dialog_color;
color: fade(white, 50%); color: darken(white, 50%);
font-size: 13px; font-size: 13px;
box-sizing: border-box; box-sizing: border-box;
border-radius: $panel_radius; border-radius: $panel_radius;
@ -761,7 +761,7 @@
content: ' '; content: ' ';
width: 100%; width: 100%;
height: 40px; height: 40px;
background: linear-gradient(fade($title_dialog_color, 0), $title_dialog_color); background: linear-gradient(darken($title_dialog_color, 0), $title_dialog_color);
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;

View file

@ -41,7 +41,7 @@
pointer-events: none; pointer-events: none;
text-transform: uppercase; text-transform: uppercase;
font-size: 1.2em; font-size: 1.2em;
color: fade(white, 70%); color: darken(white, 70%);
svg { svg {
fill: white; fill: white;

View file

@ -1,258 +0,0 @@
import axios from 'axios/index';
import { API } from '~/constants/api';
import { IRootState, IRouteListItem } from '~/redux/user';
import { IUser } from '~/constants/auth';
import { CLIENT } from '~/config/frontend';
import { LatLngLiteral } from 'leaflet';
import {
resultMiddleware,
errorMiddleware,
IResultWithStatus,
configWithToken,
} from './middleware';
import { IRoute } from '~/redux/map/types';
import { INominatimResult } from '~/redux/types';
import { MainMap } from '~/constants/map';
const arrayToObject = (array: any[], key: string): {} =>
array.reduce((obj, el) => ({ ...obj, [el[key]]: el }), {});
interface IGetRouteList {
min: number;
max: number;
tab: string;
search: string;
step: IRootState['routes']['step'];
shift: IRootState['routes']['step'];
token: IRootState['user']['token'];
}
interface IGetRouteListResult {
min: IRootState['routes']['filter']['min'];
max: IRootState['routes']['filter']['max'];
limit: IRootState['routes']['limit'];
step: IRootState['routes']['step'];
shift: IRootState['routes']['shift'];
list: IRootState['routes']['list'];
}
export const checkUserToken = ({
id,
token,
}: {
id: IRootState['user']['id'];
token: IRootState['user']['token'];
}): Promise<IResultWithStatus<{
user: IUser;
random_url: string;
routes: IRouteListItem[];
}>> =>
axios
.get(API.CHECK_TOKEN, {
params: { id, token },
})
.then(resultMiddleware)
.catch(errorMiddleware);
export const getGuestToken = (): Promise<IResultWithStatus<{
user: IUser;
random_url: string;
}>> =>
axios
.get(API.GET_GUEST)
.then(resultMiddleware)
.catch(errorMiddleware);
export const getStoredMap = ({
name,
}: {
name: IRoute['address'];
}): Promise<IResultWithStatus<{
route: IRoute;
error?: string;
random_url: string;
}>> =>
axios
.get(API.GET_MAP, {
params: { name },
})
.then(resultMiddleware)
.catch(errorMiddleware);
export const postMap = ({
title,
address,
route,
stickers,
force,
logo,
distance,
provider,
is_public,
description,
token,
}: Partial<IRoute> & {
force: boolean;
token: string;
}): Promise<IResultWithStatus<{
route: IRoute;
error?: string;
code?: string;
}>> =>
axios
.post(
API.POST_MAP,
{
route: {
title,
address,
route,
stickers,
logo,
distance,
provider,
is_public,
description,
},
force,
},
configWithToken(token)
)
.then(resultMiddleware)
.catch(errorMiddleware);
export const checkIframeToken = ({
viewer_id,
auth_key,
}: {
viewer_id: string;
auth_key: string;
}) =>
axios
.get(API.IFRAME_LOGIN_VK, {
params: { viewer_id, auth_key },
})
.then(result => result && result.data && result.data.success && result.data.user)
.catch(() => false);
export const getRouteList = ({
search,
min,
max,
tab,
token,
step,
shift,
}: IGetRouteList): Promise<IResultWithStatus<{
routes: IRoute[];
limits: {
min: number;
max: number;
count: number;
};
filter: {
min: number;
max: number;
shift: number;
step: number;
};
}>> =>
axios
.get(
API.GET_ROUTE_LIST(tab),
configWithToken(token, {
params: {
search,
min,
max,
token,
step,
shift,
},
})
)
.then(resultMiddleware)
.catch(errorMiddleware);
export const checkOSRMService = (bounds: LatLngLiteral[]): Promise<boolean> =>
CLIENT &&
CLIENT.OSRM_URL &&
axios
.get(CLIENT.OSRM_TEST_URL(bounds))
.then(() => true)
.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',
dedupe: 1,
},
})
.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> =>
axios
.delete(API.DROP_ROUTE, configWithToken(token, { data: { address } }))
.then(resultMiddleware)
.catch(errorMiddleware);
export const modifyRoute = ({
address,
token,
title,
is_public,
}: {
address: string;
token: string;
title: string;
is_public: boolean;
}): Promise<IResultWithStatus<{
route: IRoute;
}>> =>
axios
.patch(API.MODIFY_ROUTE, { address, token, is_public, title }, configWithToken(token))
.then(resultMiddleware)
.catch(errorMiddleware);
export const sendRouteStarred = ({
token,
address,
is_published,
}: {
token: string;
address: string;
is_published: boolean;
}): Promise<IResultWithStatus<{ route: IRoute }>> =>
axios
.post(API.SET_STARRED, { address, is_published }, configWithToken(token))
.then(resultMiddleware)
.catch(errorMiddleware);

180
src/utils/api/index.ts Normal file
View file

@ -0,0 +1,180 @@
import { API } from '~/constants/api';
import { IRootState } from '~/redux/user';
import { IUser } from '~/constants/auth';
import { CLIENT } from '~/config/frontend';
import { LatLngLiteral } from 'leaflet';
import { IRoute } from '~/redux/map/types';
import { INominatimResult } from '~/redux/types';
import { api } from './instance';
import { postMapInterceptor } from '~/utils/api/interceptors';
import {
CheckTokenRequest,
CheckTokenResult,
GetGuestTokenResult, GetRouteListRequest, GetRouteListResponse, GetStoredMapRequest, GetStoredMapResult,
PostMapRequest,
PostMapResponse,
} from '~/utils/api/types';
export const checkUserToken = ({
id,
token,
}: CheckTokenRequest) =>
api
.get<CheckTokenResult>(API.CHECK_TOKEN, {
params: { id, token },
});
export const getGuestToken = () =>
api
.get<GetGuestTokenResult>(API.GET_GUEST);
export const getStoredMap = ({
name,
}: GetStoredMapRequest) =>
api
.get<GetStoredMapResult>(API.GET_MAP, {
params: { name },
});
export const postMap = ({
title,
address,
route,
stickers,
force,
logo,
distance,
provider,
is_public,
description,
}: PostMapRequest) =>
api
.post<PostMapResponse>(
API.POST_MAP,
{
route: {
title,
address,
route,
stickers,
logo,
distance,
provider,
is_public,
description,
},
force,
},
).catch(postMapInterceptor);
export const checkIframeToken = ({
viewer_id,
auth_key,
}: {
viewer_id: string;
auth_key: string;
}) =>
api
.get<{
success: boolean,
user: IUser,
}>(API.IFRAME_LOGIN_VK, {
params: { viewer_id, auth_key },
})
.then(result => !!result.data.success && !!result.data.user)
.catch(() => false);
export const getRouteList = ({
search,
min,
max,
tab,
step,
shift,
}: GetRouteListRequest) =>
api
.get<GetRouteListResponse>(
API.GET_ROUTE_LIST(tab),
{
params: {
search,
min,
max,
step,
shift,
},
},
);
export const checkOSRMService = (bounds: LatLngLiteral[]) =>
!!CLIENT &&
!!CLIENT.OSRM_URL &&
api
.get<boolean>(CLIENT.OSRM_TEST_URL(bounds))
.then(() => true)
.catch(() => false);
export const checkNominatimService = () =>
!!CLIENT &&
!!CLIENT.NOMINATIM_TEST_URL &&
api
.get<boolean>(CLIENT.NOMINATIM_TEST_URL)
.then(() => true)
.catch(() => false);
export const searchNominatim = (query: string) =>
CLIENT &&
CLIENT.NOMINATIM_URL &&
api
.get(`${CLIENT.NOMINATIM_URL} ${query}`, {
params: {
format: 'json',
country_code: 'ru',
'accept-language': 'ru_RU',
dedupe: 1,
},
})
.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 }: { address: string }) =>
api
.delete(API.DROP_ROUTE, { data: { address } });
export const modifyRoute = ({
address,
title,
is_public,
}: {
address: string;
title: string;
is_public: boolean;
}) =>
api
.patch<{
route: IRoute;
}>(API.MODIFY_ROUTE, { address, is_public, title });
export const sendRouteStarred = ({
address,
is_published,
}: {
address: string;
is_published: boolean;
}) =>
api
.post<{ route: IRoute }>(API.SET_STARRED, { address, is_published });

View file

@ -0,0 +1,6 @@
import axios from 'axios';
import { CLIENT } from '~/config/frontend';
export const api = axios.create({
baseURL: CLIENT.API_ADDR,
})

View file

@ -0,0 +1,14 @@
import { AxiosError } from 'axios';
import { PostMapResponse } from '~/utils/api/types';
export const postMapInterceptor = (res: AxiosError<PostMapResponse>) => {
if (res.response?.data.code) {
return res.response;
}
if (res.response?.data.error) {
throw new Error(res.response?.data.error);
}
throw res;
};

63
src/utils/api/types.ts Normal file
View file

@ -0,0 +1,63 @@
import { IRoute } from '~/redux/map/types';
import { IUser } from '~/constants/auth';
import { IRootState, IRouteListItem } from '~/redux/user';
export interface PostMapResponse {
route: IRoute;
error?: string;
code?: string;
}
export type PostMapRequest = Partial<IRoute> & {
force: boolean;
}
export interface CheckTokenResult {
user: IUser;
random_url: string;
routes: IRouteListItem[];
}
export interface CheckTokenRequest {
id: IRootState['user']['id'];
token: string,
}
export interface GetGuestTokenResult {
user: IUser;
random_url: string;
}
export interface GetStoredMapResult {
route: IRoute;
error?: string;
random_url: string;
}
export interface GetStoredMapRequest {
name: IRoute['address'];
}
export interface GetRouteListRequest {
min: number;
max: number;
tab: string;
search: string;
step: IRootState['routes']['step'];
shift: IRootState['routes']['step'];
}
export interface GetRouteListResponse {
routes: IRoute[];
limits: {
min: number;
max: number;
count: number;
};
filter: {
min: number;
max: number;
shift: number;
step: number;
};
}

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Map, TileLayer } from 'leaflet'; import { Map, TileLayer } from 'leaflet';
export const MapContext = React.createContext<Map>(null); export const MapContext = React.createContext<Map | undefined>(undefined);
export const TileContext = React.createContext<TileLayer>(null) export const TileContext = React.createContext<TileLayer | undefined>(undefined)

View file

@ -1,9 +1,4 @@
import { LatLng, LatLngLiteral, point, Point, PointExpression, latLng } from 'leaflet'; import { LatLng, latLng, LatLngLiteral, Point, point } from 'leaflet';
// interface LatLng {
// lat: number;
// lng: number;
// }
export const middleCoord = (l1: LatLng, l2: LatLng): LatLng => latLng({ export const middleCoord = (l1: LatLng, l2: LatLng): LatLng => latLng({
lat: l2.lat + (l1.lat - l2.lat) / 2, lat: l2.lat + (l1.lat - l2.lat) / 2,
@ -46,14 +41,14 @@ export const findDistance = (t1: number, n1: number, t2: number, n2: number): nu
export const findDistanceHaversine = (t1: number, n1: number, t2: number, n2: number): number => { export const findDistanceHaversine = (t1: number, n1: number, t2: number, n2: number): number => {
const R = 6371; // km const R = 6371; // km
const dLat = ((t2 - t1) * Math.PI) / 180; const dLat = ((t2 - t1) * Math.PI) / 180;
var dLon = ((n2 - n1) * Math.PI) / 180; const dLon = ((n2 - n1) * Math.PI) / 180;
var a = const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((t1 * Math.PI) / 180) * Math.cos((t1 * Math.PI) / 180) *
Math.cos((t2 * Math.PI) / 180) * Math.cos((t2 * Math.PI) / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2) *
Math.sin(dLon / 2); Math.sin(dLon / 2);
var c = 2 * Math.asin(Math.sqrt(a)); const c = 2 * Math.asin(Math.sqrt(a));
return R * c; return R * c;
}; };
@ -88,7 +83,7 @@ export const dist2 = (A: LatLngLiteral, B: LatLngLiteral): number =>
const distToSegmentSquared = (A: LatLng, B: LatLng, C: LatLng): number => { const distToSegmentSquared = (A: LatLng, B: LatLng, C: LatLng): number => {
const l2 = dist2(A, B); const l2 = dist2(A, B);
if (l2 == 0) return dist2(C, A); if (l2 === 0) return dist2(C, A);
const t = Math.max( const t = Math.max(
0, 0,

View file

@ -39,7 +39,7 @@ export const getGPXString = ({
<metadata> <metadata>
<name>${title || 'GPX Track'}</name> <name>${title || 'GPX Track'}</name>
</metadata> </metadata>
${stickers.reduce( ${(stickers || []).reduce(
(cat, { latlng: { lat, lng }, text }) => (cat, { latlng: { lat, lng }, text }) =>
`${cat} `${cat}
<wpt lat="${lat}" lon="${lng}"> <wpt lat="${lat}" lon="${lng}">
@ -93,12 +93,12 @@ export const importGpxTrack = async (file: File) => {
return trkseg.trkpt return trkseg.trkpt
? [ ? [
...trkseg_res, ...trkseg_res,
...trkseg.trkpt.map(pnt => ({ lat: pnt['$'].lat, lng: pnt['$'].lon })), ...trkseg.trkpt.map(pnt => new LatLng(pnt['$'].lat, pnt['$'].lon)),
] ]
: trkseg_res; : trkseg_res;
}, trk_res) }, trk_res)
: trk_res; : trk_res;
}, []); }, [] as LatLng[]);
return [ return [
{ {

View file

@ -1,13 +1,5 @@
import { history } from '~/redux/store'; import { history } from '~/redux/store';
import {API_RETRY_INTERVAL} from "~/constants/api"; import { API_RETRY_INTERVAL } from '~/constants/api';
interface IUrlData {
path: string,
mode: 'edit' | '',
host: string,
hash: string,
protocol: 'http' | 'https',
}
export const getPath = (): string => (window.location && window.location.pathname); export const getPath = (): string => (window.location && window.location.pathname);
export const pushPath = (url: string): string => history.push(url); export const pushPath = (url: string): string => history.push(url);
@ -36,20 +28,20 @@ export const parseQuery = (queryString: string) => {
}; };
export const pushLoaderState = (state: number) => { export const pushLoaderState = (state: number) => {
document.getElementById('loader-bar').style.width = `${state}%`; document.getElementById('loader-bar')!.style.width = `${state}%`;
}; };
export const countDownToRefresh = (left: number = API_RETRY_INTERVAL): void => { export const countDownToRefresh = (left: number = API_RETRY_INTERVAL): void => {
if (left <= 0) return document.location.reload(); if (left <= 0) return document.location.reload();
document.getElementById('loader-bar').style.width = `${(left / API_RETRY_INTERVAL) * 100}%`; document.getElementById('loader-bar')!.style.width = `${(left / API_RETRY_INTERVAL) * 100}%`;
setTimeout(() => countDownToRefresh(left - 0.25), 1000); setTimeout(() => countDownToRefresh(left - 0.25), 1000);
}; };
export const pushNetworkInitError = () => { export const pushNetworkInitError = () => {
document.getElementById('loader-bar').classList.add('is_failed'); document.getElementById('loader-bar')!.classList.add('is_failed');
document.getElementById('loader-bar').style.width = '100%'; document.getElementById('loader-bar')!.style.width = '100%';
document.getElementById('loader-error').style.opacity = String(1); document.getElementById('loader-error')!.style.opacity = String(1);
countDownToRefresh(); countDownToRefresh();
}; };

View file

@ -9,6 +9,7 @@ import { LatLng, LatLngLiteral, LayerGroup, Map, Marker } from 'leaflet';
import { arrowClusterIcon, createArrow } from '~/utils/arrow'; import { arrowClusterIcon, createArrow } from '~/utils/arrow';
import { MarkerClusterGroup } from 'leaflet.markercluster/dist/leaflet.markercluster-src.js'; import { MarkerClusterGroup } from 'leaflet.markercluster/dist/leaflet.markercluster-src.js';
import { angleBetweenPoints, dist2, middleCoord } from '~/utils/geom'; import { angleBetweenPoints, dist2, middleCoord } from '~/utils/geom';
import { MainMap } from '~/constants/map';
class ArrowsLayer extends LayerGroup { class ArrowsLayer extends LayerGroup {
constructor(props) { constructor(props) {
@ -46,13 +47,13 @@ class ArrowsLayer extends LayerGroup {
), ),
] ]
: res, : res,
[] [] as Marker[]
); );
this.arrowLayer.addLayers(midpoints); this.arrowLayer.addLayers(midpoints);
}; };
map: Map; map: Map = MainMap;
arrowLayer = new MarkerClusterGroup({ arrowLayer = new MarkerClusterGroup({
spiderfyOnMaxZoom: false, spiderfyOnMaxZoom: false,
showCoverageOnHover: false, showCoverageOnHover: false,
@ -62,10 +63,10 @@ class ArrowsLayer extends LayerGroup {
iconCreateFunction: arrowClusterIcon, iconCreateFunction: arrowClusterIcon,
}); });
layers: Marker<any>[] = []; layers: Marker[] = [];
} }
ArrowsLayer.addInitHook(function() { ArrowsLayer.addInitHook(function(this: ArrowsLayer) {
this.once('add', event => { this.once('add', event => {
if (event.target instanceof ArrowsLayer) { if (event.target instanceof ArrowsLayer) {
this.map = event.target._map; this.map = event.target._map;

View file

@ -36,9 +36,9 @@ class InteractivePoly extends Polyline {
this.constraintsStyle = { this.constraintsStyle = {
...this.constraintsStyle, ...this.constraintsStyle,
...options.constraintsStyle, ...(options?.constraintsStyle || {}),
}; };
this.maxMarkers = options.maxMarkers || this.maxMarkers; this.maxMarkers = options?.maxMarkers || this.maxMarkers;
this.constrLine = new Polyline([], this.constraintsStyle); this.constrLine = new Polyline([], this.constraintsStyle);
@ -162,10 +162,12 @@ class InteractivePoly extends Polyline {
? { ...obj, hidden: [...obj.hidden, marker] } ? { ...obj, hidden: [...obj.hidden, marker] }
: { ...obj, visible: [...obj.visible, marker] }; : { ...obj, visible: [...obj.visible, marker] };
}, },
{ visible: [], hidden: [] } { visible: [], hidden: [] } as Record<'visible' | 'hidden', Marker[]>
); );
if (visible.length > this.maxMarkers) return this.hideAllMarkers(); if (visible.length > (this.maxMarkers || 2)) {
return this.hideAllMarkers();
}
this.showAllMarkers(); this.showAllMarkers();
@ -337,11 +339,11 @@ class InteractivePoly extends Polyline {
onMarkerDrag = ({ target }: { target: Marker }) => { onMarkerDrag = ({ target }: { target: Marker }) => {
const coords = new Array(0) const coords = new Array(0)
.concat((this.vertex_index > 0 && this.markers[this.vertex_index - 1].getLatLng()) || []) .concat((this.vertex_index! > 0 && this.markers[this.vertex_index! - 1].getLatLng()) || [])
.concat(target.getLatLng()) .concat(target.getLatLng())
.concat( .concat(
(this.vertex_index < this.markers.length - 1 && (this.vertex_index! < this.markers.length - 1 &&
this.markers[this.vertex_index + 1].getLatLng()) || this.markers[this.vertex_index! + 1].getLatLng()) ||
[] []
); );
@ -369,17 +371,17 @@ class InteractivePoly extends Polyline {
onMarkerDragEnd = ({ target }: { target: Marker }): void => { onMarkerDragEnd = ({ target }: { target: Marker }): void => {
const latlngs = this.getLatLngs() as LatLngLiteral[]; const latlngs = this.getLatLngs() as LatLngLiteral[];
this.markerDragChangeDistance( this.markerDragChangeDistance(
this.vertex_index, this.vertex_index!,
latlngs[this.vertex_index], latlngs[this.vertex_index!],
target.getLatLng() target.getLatLng()
); );
this.replaceLatlng(target.getLatLng(), this.vertex_index); this.replaceLatlng(target.getLatLng(), this.vertex_index!);
this.is_dragging = false; this.is_dragging = false;
this.constrLine.removeFrom(this._map); this.constrLine.removeFrom(this._map);
this.vertex_index = null; this.vertex_index = 0;
if (this.is_drawing) this.startDrawing(); if (this.is_drawing) this.startDrawing();
@ -505,7 +507,7 @@ class InteractivePoly extends Polyline {
const index = this.markers.indexOf(target); const index = this.markers.indexOf(target);
const latlngs = this.getLatLngs(); const latlngs = this.getLatLngs();
if (typeof index === 'undefined' || latlngs.length == 0) return; if (typeof index === 'undefined' || latlngs.length === 0) return;
this.dropMarkerDistanceChange(index); this.dropMarkerDistanceChange(index);
this._map.removeLayer(this.markers[index]); this._map.removeLayer(this.markers[index]);
@ -568,27 +570,27 @@ class InteractivePoly extends Polyline {
is_drawing: boolean = false; is_drawing: boolean = false;
drawing_direction: 'forward' | 'backward' = 'forward'; drawing_direction: 'forward' | 'backward' = 'forward';
vertex_index?: number = null; vertex_index: number = 0;
hint_prev_marker: number = null; hint_prev_marker: number = 0;
distance: number = 0; distance: number = 0;
} }
InteractivePoly.addInitHook(function() { InteractivePoly.addInitHook(function(this: InteractivePoly) {
this.once('add', event => { this.once('add', event => {
if (event.target instanceof InteractivePoly) { if (event.target instanceof InteractivePoly) {
this.map = event.target._map; this._map = event.target._map;
this.map.on('touch', console.log); this._map.on('touch', console.log);
this.markerLayer.addTo(event.target._map); this.markerLayer.addTo(event.target._map);
this.hintMarker.addTo(event.target._map); this.hintMarker.addTo(event.target._map);
this.constrLine.addTo(event.target._map); this.constrLine.addTo(event.target._map);
this.touchHinter.addTo(event.target._map); this.touchHinter.addTo(event.target._map);
this.map.on('moveend', this.updateMarkers); this._map.on('moveend', this.updateMarkers);
this.on('latlngschange', this.updateTouchHinter); this.on('latlngschange' as any, this.updateTouchHinter as any);
if (this.touchHinter && window.innerWidth < 768) { if (this.touchHinter && window.innerWidth < 768) {
try { try {
@ -605,7 +607,7 @@ InteractivePoly.addInitHook(function() {
this.constrLine.removeFrom(this._map); this.constrLine.removeFrom(this._map);
this.touchHinter.removeFrom(this._map); this.touchHinter.removeFrom(this._map);
this.map.off('moveend', this.updateMarkers); this._map.off('moveend', this.updateMarkers);
} }
}); });
}); });

View file

@ -35,7 +35,7 @@ export const OsrmRouter = Routing.control({
show: false, show: false,
plan: Routing.plan([], { plan: Routing.plan([], {
createMarker: (_, wp) => { createMarker: (_, wp) => {
const marker = new Marker(wp.latLng, { return new Marker(wp.latLng, {
draggable: true, draggable: true,
icon: createWaypointMarker(), icon: createWaypointMarker(),
}) })
@ -45,12 +45,10 @@ export const OsrmRouter = Routing.control({
OsrmRouter.setWaypoints( OsrmRouter.setWaypoints(
OsrmRouter.getWaypoints().filter( OsrmRouter.getWaypoints().filter(
point => point =>
!point.latLng || (point.latLng.lat != latlng.lat && point.latLng.lng != latlng.lng) !point.latLng || (point.latLng.lat !== latlng.lat && point.latLng.lng !== latlng.lng)
) )
); );
}); });
return marker;
}, },
routeWhileDragging: false, routeWhileDragging: false,
}), }),

View file

@ -3,6 +3,7 @@ import { arrowClusterIcon } from '~/utils/arrow';
import { MarkerClusterGroup } from 'leaflet.markercluster/dist/leaflet.markercluster-src.js'; import { MarkerClusterGroup } from 'leaflet.markercluster/dist/leaflet.markercluster-src.js';
import { allwaysPositiveAngleDeg, angleBetweenPoints, distKmHaversine } from '~/utils/geom'; import { allwaysPositiveAngleDeg, angleBetweenPoints, distKmHaversine } from '~/utils/geom';
import classNames from 'classnames'; import classNames from 'classnames';
import { MainMap } from '~/constants/map';
const arrow_image = '/images/arrow.svg'; const arrow_image = '/images/arrow.svg';
@ -43,8 +44,7 @@ class KmMarksLayer extends LayerGroup {
}; };
drawMiddleMarkers = (latlngs: LatLngLiteral[]) => { drawMiddleMarkers = (latlngs: LatLngLiteral[]) => {
const marks = []; const marks: Marker[] = [];
const arrows = [];
let last_km_mark = 0; let last_km_mark = 0;
this.distance = latlngs.reduce((dist, current, index) => { this.distance = latlngs.reduce((dist, current, index) => {
@ -160,7 +160,7 @@ class KmMarksLayer extends LayerGroup {
}; };
options: KmMarksOptions; options: KmMarksOptions;
map: Map; map: Map = MainMap;
marksLayer: MarkerClusterGroup = new MarkerClusterGroup({ marksLayer: MarkerClusterGroup = new MarkerClusterGroup({
spiderfyOnMaxZoom: false, spiderfyOnMaxZoom: false,
showCoverageOnHover: false, showCoverageOnHover: false,
@ -173,7 +173,7 @@ class KmMarksLayer extends LayerGroup {
distance: number = 0; distance: number = 0;
} }
KmMarksLayer.addInitHook(function() { KmMarksLayer.addInitHook(function(this: KmMarksLayer) {
this.once('add', event => { this.once('add', event => {
if (event.target instanceof KmMarksLayer) { if (event.target instanceof KmMarksLayer) {
this.map = event.target._map; this.map = event.target._map;

View file

@ -1,4 +1,4 @@
import { AxiosRequestConfig } from "axios"; import { AxiosRequestConfig, AxiosResponse } from 'axios';
export type Unwrap<T> = T extends (...args: any[]) => Promise<infer U> ? U : T; export type Unwrap<T> = T extends (...args: any[]) => Promise<infer U> ? U : T;
@ -23,28 +23,3 @@ export const HTTP_RESPONSES = {
NOT_FOUND: 404, NOT_FOUND: 404,
TOO_MANY_REQUESTS: 429, TOO_MANY_REQUESTS: 429,
}; };
export const resultMiddleware = (<T extends {}>({
status,
data,
}: {
status: number;
data: T;
}): { status: number; data: T } => ({ status, data }));
export const errorMiddleware = <T extends any>(debug): IResultWithStatus<T> => (debug && debug.response
? debug.response
: {
status: HTTP_RESPONSES.CONNECTION_REFUSED,
data: {},
debug,
error: 'Ошибка сети',
});
export const configWithToken = (
token: string,
config: AxiosRequestConfig = {},
): AxiosRequestConfig => ({
...config,
headers: { ...(config.headers || {}), Authorization: `${token}` },
});

View file

@ -1,10 +1,3 @@
// create-reducer.ts
import { Action } from 'redux';
type Handlers<State, Types extends string, Actions extends Action<Types>> = {
readonly [Type in Types]: (state: State, action: Actions) => State
}
export const createReducer = ( export const createReducer = (
initialState, initialState,
handlers, handlers,

View file

@ -1,18 +1,10 @@
// import { editor } from '~/modules/Editor'; import { CLIENT, COLORS } from '~/config/frontend';
import { COLORS, CLIENT } from '~/config/frontend';
import saveAs from 'file-saver'; import saveAs from 'file-saver';
import { replaceProviderUrl } from '~/constants/providers'; import { replaceProviderUrl } from '~/constants/providers';
import { STICKERS } from '~/constants/stickers'; import { STICKERS } from '~/constants/stickers';
import { IRoute } from '~/redux/map/types'; import { IRoute, IStickerDump } from '~/redux/map/types';
import { IStickerDump } from '~/redux/map/types'; import { angleBetweenPoints, angleBetweenPointsRad, findDistancePx, middleCoordPx } from '~/utils/geom';
import { IRootState } from '~/redux/user'; import { LatLng, latLng, Point } from 'leaflet';
import {
angleBetweenPoints,
angleBetweenPointsRad,
findDistancePx,
middleCoordPx,
} from '~/utils/geom';
import { Point, LatLng, latLng } from 'leaflet';
import { MainMap } from '~/constants/map'; import { MainMap } from '~/constants/map';
export interface ITilePlacement { export interface ITilePlacement {
@ -37,8 +29,7 @@ const latLngToTile = (latlng: {
lat: number; lat: number;
lng: number; lng: number;
}): { x: number; y: number; z: number } => { }): { x: number; y: number; z: number } => {
const map = MainMap; const zoom = MainMap.getZoom();
const zoom = map.getZoom();
const xtile = Number(Math.floor(((latlng.lng + 180) / 360) * (1 << zoom))); const xtile = Number(Math.floor(((latlng.lng + 180) / 360) * (1 << zoom)));
const ytile = Number( const ytile = Number(
Math.floor( Math.floor(
@ -56,8 +47,7 @@ const latLngToTile = (latlng: {
}; };
const tileToLatLng = (point: { x: number; y: number }): LatLng => { const tileToLatLng = (point: { x: number; y: number }): LatLng => {
const map = MainMap; const z = MainMap.getZoom();
const z = map.getZoom();
const lng = (point.x / Math.pow(2, z)) * 360 - 180; const lng = (point.x / Math.pow(2, z)) * 360 - 180;
const n = Math.PI - (2 * Math.PI * point.y) / Math.pow(2, z); const n = Math.PI - (2 * Math.PI * point.y) / Math.pow(2, z);
const lat = (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); const lat = (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
@ -133,7 +123,7 @@ export const fetchImages = (
): Promise<{ x: number; y: number; image: HTMLImageElement }[]> => { ): Promise<{ x: number; y: number; image: HTMLImageElement }[]> => {
const { minX, maxX, minY, maxY, zoom } = geometry; const { minX, maxX, minY, maxY, zoom } = geometry;
const images = []; const images: { x: number; y: number; source: string }[] = [];
for (let x = minX; x <= maxX; x += 1) { for (let x = minX; x <= maxX; x += 1) {
for (let y = minY; y <= maxY; y += 1) { for (let y = minY; y <= maxY; y += 1) {
images.push({ x, y, source: getImageSource({ x, y, zoom }, provider) }); images.push({ x, y, source: getImageSource({ x, y, zoom }, provider) });
@ -173,7 +163,7 @@ export const composePoly = ({
ctx, ctx,
color = 'gradient', color = 'gradient',
weight = CLIENT.STROKE_WIDTH, weight = CLIENT.STROKE_WIDTH,
dash = null, dash = [],
}: { }: {
points: Point[]; points: Point[];
ctx: CanvasRenderingContext2D; ctx: CanvasRenderingContext2D;
@ -217,7 +207,7 @@ export const composePoly = ({
} }
if (dash) { if (dash) {
ctx.setLineDash([12, 12]); ctx.setLineDash(dash);
} }
ctx.stroke(); ctx.stroke();
@ -472,14 +462,14 @@ export const composeStickers = async ({
if (!stickers || stickers.length < 0) return; if (!stickers || stickers.length < 0) return;
stickers.map(({ x, y, angle, text }) => { stickers.map(({ x, y, angle, text }) => {
composeStickerArrow(ctx, x, y, angle, zoom); composeStickerArrow(ctx, x, y, angle || 0, zoom);
if (text) composeStickerText(ctx, x, y, angle, text, zoom); if (text) composeStickerText(ctx, x, y, angle || 0, text, zoom);
}); });
await Promise.all( await Promise.all(
stickers.map(({ x, y, angle, set, sticker }) => stickers.map(({ x, y, angle, set, sticker }) =>
composeStickerImage(ctx, x, y, angle, set, sticker, zoom) composeStickerImage(ctx, x, y, angle || 0, set, sticker, zoom)
) )
); );
}; };

View file

@ -1,4 +1,4 @@
import { Map, LineUtil, LatLng } from 'leaflet'; import { LatLng, LineUtil } from 'leaflet';
import { MainMap } from '~/constants/map'; import { MainMap } from '~/constants/map';
export const simplify = (latlngs: LatLng[]): LatLng[] => { export const simplify = (latlngs: LatLng[]): LatLng[] => {

View file

@ -3,10 +3,10 @@ import { LatLngLiteral } from 'leaflet';
export const isMobile = (): boolean => window.innerWidth <= MOBILE_BREAKPOINT; export const isMobile = (): boolean => window.innerWidth <= MOBILE_BREAKPOINT;
export const getLocation = (callback: (pos: LatLngLiteral) => void) => { export const getLocation = (callback: (pos: LatLngLiteral | undefined) => void) => {
window.navigator.geolocation.getCurrentPosition(position => { window.navigator.geolocation.getCurrentPosition(position => {
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(undefined);
const { latitude: lat, longitude: lng } = position.coords; const { latitude: lat, longitude: lng } = position.coords;
@ -15,18 +15,18 @@ export const getLocation = (callback: (pos: LatLngLiteral) => void) => {
}); });
}; };
export const watchLocation = (callback: (pos: LatLngLiteral) => void): number => { export const watchLocation = (callback: (pos: LatLngLiteral | undefined) => void): number => {
return window.navigator.geolocation.watchPosition( return window.navigator.geolocation.watchPosition(
position => { position => {
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(undefined);
const { latitude: lat, longitude: lng } = position.coords; const { latitude: lat, longitude: lng } = position.coords;
callback({ lat, lng }); callback({ lat, lng });
return; return;
}, },
() => callback(null), () => callback(undefined),
{ {
timeout: 30, timeout: 30,
} }

View file

@ -11,7 +11,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": false, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"module": "esnext", "module": "esnext",

View file

@ -7197,13 +7197,6 @@ leaflet-editable@^1.1.0:
resolved "https://registry.yarnpkg.com/leaflet-editable/-/leaflet-editable-1.2.0.tgz#a3a01001764ba58ea923381ee6a1c814708a0b84" resolved "https://registry.yarnpkg.com/leaflet-editable/-/leaflet-editable-1.2.0.tgz#a3a01001764ba58ea923381ee6a1c814708a0b84"
integrity sha512-wG11JwpL8zqIbypTop6xCRGagMuWw68ihYu4uqrqc5Ep0wnEJeyob7NB2Rt5t74Oih4rwJ3OfwaGbzdowOGfYQ== integrity sha512-wG11JwpL8zqIbypTop6xCRGagMuWw68ihYu4uqrqc5Ep0wnEJeyob7NB2Rt5t74Oih4rwJ3OfwaGbzdowOGfYQ==
leaflet-geometryutil@^0.9.0:
version "0.9.3"
resolved "https://registry.yarnpkg.com/leaflet-geometryutil/-/leaflet-geometryutil-0.9.3.tgz#e10fa302d99d4b1d3c6365a1f39298635a2704cd"
integrity sha512-Wi6YvfNx/Xu9q35AEfXpsUXmIFLen/MO+C2qimxHRnjyeyOxBhdcZa6kSiReaOX0cGK7yQInqrzz0dkIqZ8Dpg==
dependencies:
leaflet ">=0.7.0"
leaflet-routing-machine@^3.2.12: leaflet-routing-machine@^3.2.12:
version "3.2.12" version "3.2.12"
resolved "https://registry.yarnpkg.com/leaflet-routing-machine/-/leaflet-routing-machine-3.2.12.tgz#9e4aef008321b0227cf894d829c3b4c1f13e4e13" resolved "https://registry.yarnpkg.com/leaflet-routing-machine/-/leaflet-routing-machine-3.2.12.tgz#9e4aef008321b0227cf894d829c3b4c1f13e4e13"
@ -7223,11 +7216,6 @@ leaflet@1.6.0:
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308" resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308"
integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ== integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ==
leaflet@>=0.7.0:
version "1.7.1"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19"
integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw==
leven@^3.1.0: leven@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"