added gpx dialog

This commit is contained in:
Fedor Katurov 2020-01-24 16:53:47 +07:00
parent 947ec69e60
commit e995b46641
33 changed files with 11687 additions and 131 deletions

32
package-lock.json generated
View file

@ -870,8 +870,7 @@
"@types/node": {
"version": "11.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.9.0.tgz",
"integrity": "sha512-ry4DOrC+xenhQbzk1iIPzCZGhhPGEFv7ia7Iu6XXSLVluiJIe9FfG7Iu3mObH9mpxEXCWLCMU4JWbCCR9Oy1Zg==",
"dev": true
"integrity": "sha512-ry4DOrC+xenhQbzk1iIPzCZGhhPGEFv7ia7Iu6XXSLVluiJIe9FfG7Iu3mObH9mpxEXCWLCMU4JWbCCR9Oy1Zg=="
},
"@types/prop-types": {
"version": "15.5.8",
@ -903,6 +902,14 @@
"csstype": "2.6.2"
}
},
"@types/xml2js": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.5.tgz",
"integrity": "sha512-yohU3zMn0fkhlape1nxXG2bLEGZRc1FeqF80RoHaYXJN7uibaauXfhzhOJr1Xh36sn+/tx21QAOf07b/xYVk1w==",
"requires": {
"@types/node": "11.9.0"
}
},
"@webassemblyjs/ast": {
"version": "1.7.11",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz",
@ -6766,6 +6773,14 @@
}
}
},
"gpx-parser-builder": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/gpx-parser-builder/-/gpx-parser-builder-1.0.2.tgz",
"integrity": "sha512-zCTGKANSytYLIicVYUUFTYhz3mbDEtIemWZvC3Vb0j8DhwPMbDSCIl9blMClxSLrr7gGbwLAk1nhj3Z41oC5sw==",
"requires": {
"isomorphic-xml2js": "0.1.3"
}
},
"graceful-fs": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
@ -7794,6 +7809,15 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
"isomorphic-xml2js": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/isomorphic-xml2js/-/isomorphic-xml2js-0.1.3.tgz",
"integrity": "sha512-dIkT2U9ritKVWF/HfHfGwm5tTnlMnknYsv7l12oJlQQgOV2CNV65pX+FHy6HFL9YP8q0JcrlNQAFRJIN2agUmQ==",
"requires": {
"@types/xml2js": "0.4.5",
"xml2js": "0.4.22"
}
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@ -14175,7 +14199,6 @@
"version": "0.4.22",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.22.tgz",
"integrity": "sha512-MWTbxAQqclRSTnehWWe5nMKzI3VmJ8ltiJEco8akcC6j3miOhjjfzKum5sId+CWhfxdOs/1xauYr8/ZDBtQiRw==",
"dev": true,
"requires": {
"sax": "1.2.4",
"util.promisify": "1.0.0",
@ -14185,8 +14208,7 @@
"xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"dev": true
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
},
"xregexp": {
"version": "4.0.0",

View file

@ -15,10 +15,10 @@
"devDependencies": {
"@babel/cli": "^7.0.0-rc.1",
"@babel/preset-env": "^7.0.0-rc.1",
"@types/ramda": "^0.26.39",
"@types/classnames": "^2.2.7",
"@types/leaflet": "^1.4.3",
"@types/node": "^11.9.0",
"@types/ramda": "^0.26.39",
"@types/react": "16.8.1",
"awesome-typescript-loader": "^5.2.1",
"babel-core": "^6.26.0",
@ -66,6 +66,7 @@
"croppr": "^2.3.1",
"debug": "~2.6.9",
"file-saver": "^2.0.0",
"gpx-parser-builder": "^1.0.2",
"history": "^4.7.2",
"http-errors": "~1.6.2",
"js-md5": "^0.7.3",
@ -102,6 +103,7 @@
"tt-react-custom-scrollbars": "^4.2.1-tt2",
"typeface-pt-sans": "0.0.54",
"typesafe-actions": "^3.0.0",
"uuid": "^3.4.0",
"webpack-git-hash": "^1.0.2"
}
}

View file

@ -0,0 +1,147 @@
import React, { FC, useCallback, ChangeEvent } from 'react';
import { connect } from 'react-redux';
import * as EDITOR_ACTIONS from '~/redux/editor/actions';
import { IState } from '~/redux/store';
import { selectEditorGpx } from '~/redux/editor/selectors';
import { GpxDialogRow } from '~/components/gpx/GpxDialogRow';
import { MainMap } from '~/constants/map';
import { latLngBounds } from 'leaflet';
import { Switch } from '../Switch';
import { selectMapRoute, selectMapTitle, selectMapAddress } from '~/redux/map/selectors';
import classNames from 'classnames';
import uuid from 'uuid';
import { getUrlData } from '~/utils/history';
import { getRandomColor } from '~/utils/dom';
const mapStateToProps = (state: IState) => ({
gpx: selectEditorGpx(state),
route: selectMapRoute(state),
title: selectMapTitle(state),
address: selectMapAddress(state),
});
const mapDispatchToProps = {
editorDropGpx: EDITOR_ACTIONS.editorDropGpx,
editorUploadGpx: EDITOR_ACTIONS.editorUploadGpx,
editorSetGpx: EDITOR_ACTIONS.editorSetGpx,
editorGetGPXTrack: EDITOR_ACTIONS.editorGetGPXTrack,
};
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
const GpxDialogUnconnected: FC<Props> = ({
title,
address,
gpx,
route,
editorGetGPXTrack,
editorSetGpx,
editorUploadGpx,
}) => {
const onGpxUpload = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
if (!event.target || !event.target.files || event.target.files.length === 0) {
return;
}
editorUploadGpx(event.target.files[0]);
},
[editorUploadGpx]
);
const onFocusRoute = useCallback(
index => {
if (!gpx.list[index] || !gpx.list[index].latlngs) return;
const bounds = latLngBounds(gpx.list[index].latlngs);
MainMap.fitBounds(bounds);
},
[gpx, MainMap]
);
const onRouteDrop = useCallback(
index => {
editorSetGpx({ list: gpx.list.filter((el, i) => i !== index) });
},
[gpx, editorSetGpx]
);
const toggleGpx = useCallback(() => {
editorSetGpx({ enabled: !gpx.enabled });
}, [gpx, editorSetGpx]);
const onRouteToggle = useCallback(
index => {
if (!gpx.enabled) return;
editorSetGpx({
list: gpx.list.map((el, i) => (i !== index ? el : { ...el, enabled: !el.enabled })),
});
},
[gpx, editorSetGpx]
);
const addCurrent = useCallback(() => {
if (!route.length) return;
const { path } = getUrlData()
editorSetGpx({
list: [
...gpx.list,
{
latlngs: route,
enabled: false,
name: title || address || path,
id: uuid(),
color: getRandomColor(),
},
],
});
}, [route, gpx, editorSetGpx]);
return (
<div className="control-dialog control-dialog__left control-dialog__small">
<div className="gpx-title">
<div className="flex_1 big white upper">Треки</div>
<Switch active={gpx.enabled} onPress={toggleGpx} />
</div>
{gpx.list.map((item, index) => (
<GpxDialogRow
item={item}
key={item.id}
index={index}
enabled={gpx.enabled}
onRouteDrop={onRouteDrop}
onFocusRoute={onFocusRoute}
onRouteToggle={onRouteToggle}
/>
))}
<div className="gpx-buttons">
<button className="button outline">
<input type="file" onChange={onGpxUpload} />
Загрузить GPX
</button>
<div
className={classNames('button outline', { disabled: !route.length })}
onClick={addCurrent}
>
Добавить текущий
</div>
<div
className={classNames('button success', { disabled: !route.length })}
onClick={editorGetGPXTrack}
>
Скачать текущий
</div>
</div>
</div>
);
};
const GpxDialog = connect(mapStateToProps, mapDispatchToProps)(GpxDialogUnconnected);
export { GpxDialog };

View file

@ -0,0 +1,34 @@
import React, { FC } from 'react';
import { IGpxRoute } from '~/redux/editor';
import { Switch } from '../Switch';
import { Icon } from '../panels/Icon';
import classnames from 'classnames';
interface IProps {
item: IGpxRoute;
index: number
enabled: boolean;
onFocusRoute: (i: number) => void
onRouteDrop: (i: number) => void
onRouteToggle: (i: number) => void
}
const GpxDialogRow: FC<IProps> = ({ item, index, enabled, onRouteToggle, onFocusRoute, onRouteDrop }) => {
return (
<div className={classnames("gpx-row", { 'gpx-row_disabled': !enabled || !item.enabled })}>
<div className="gpx-row__color" style={{ backgroundColor: item.color }}/>
<div className="gpx-row__title" onClick={() => onFocusRoute(index)}>
{item.name}
</div>
<div className="gpx-row__buttons">
<div onClick={() => onRouteDrop(index)}><Icon icon="icon-trash-6" size={24} /></div>
<div><Switch active={item.enabled} onPress={() => onRouteToggle(index)}/></div>
</div>
</div>
);
};
export { GpxDialogRow };

View file

@ -8,6 +8,7 @@ import { TrashDialog } from '~/components/dialogs/TrashDialog';
import { LogoDialog } from '~/components/dialogs/LogoDialog';
import { SaveDialog } from '~/components/dialogs/SaveDialog';
import { CancelDialog } from '~/components/dialogs/CancelDialog';
import { GpxDialog } from '~/components/dialogs/GpxDialog';
import { connect } from 'react-redux';
@ -31,6 +32,7 @@ const DIALOG_CONTENTS: { [x: string]: any } = {
[MODES.PROVIDER]: ProviderDialog,
[MODES.SHOT_PREFETCH]: ShotPrefetchDialog,
[MODES.POLY]: PolylineDialog,
[MODES.GPX]: GpxDialog,
};
const EditorDialogUnconnected = (props: Props) =>

View file

@ -11,6 +11,7 @@ import {
editorSetDialogActive,
editorGetGPXTrack,
editorSearchNominatim,
editorChangeMode,
} from '~/redux/editor/actions';
import { connect } from 'react-redux';
import { Icon } from '~/components/panels/Icon';
@ -22,6 +23,7 @@ import { Tooltip } from '~/components/panels/Tooltip';
import { TitleDialog } from '~/components/dialogs/TitleDialog';
import { NominatimSearchPanel } from '~/components/dialogs/NominatimSearchPanel';
import { IState } from '~/redux/store';
import { MODES } from '~/constants/modes';
const mapStateToProps = ({
user: { user },
@ -46,6 +48,7 @@ const mapDispatchToProps = {
openMapDialog,
editorGetGPXTrack,
editorSearchNominatim,
editorChangeMode,
};
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
@ -118,13 +121,17 @@ export class UserPanelUnconnected extends PureComponent<Props, State> {
);
};
openGpxDialog = () => {
this.props.editorChangeMode(MODES.GPX);
}
render() {
const {
props: { user, dialog, dialog_active, route, stickers, features },
state: { menuOpened },
} = this;
const is_empty = !route.length && !stickers.length;
// const is_empty = !route.length && !stickers.length;
return (
<div>
@ -171,8 +178,9 @@ export class UserPanelUnconnected extends PureComponent<Props, State> {
<div className="control-bar">
<button
className={classnames({ inactive: is_empty })}
onClick={this.props.editorGetGPXTrack}
// className={classnames({ inactive: is_empty })}
onClick={this.openGpxDialog}
// onClick={this.props.editorGetGPXTrack}
>
<Tooltip>Экспорт GPX</Tooltip>
<Icon icon="icon-gpx-1" />

View file

@ -12,4 +12,5 @@ export const MODES = {
CONFIRM_CANCEL: 'CONFIRM_CANCEL',
PROVIDER: 'PROVIDER',
SHOT_PREFETCH: 'SHOT_PREFETCH',
GPX: 'GPX',
};

View file

@ -0,0 +1,35 @@
import { FC, useEffect, useState } from 'react';
import { Polyline, LatLngLiteral } from 'leaflet';
import { MainMap } from '~/constants/map';
interface IProps {
latlngs: LatLngLiteral[];
color: string;
}
const GpxPolyline: FC<IProps> = ({ latlngs, color }) => {
const [layer, setLayer] = useState<Polyline>(null);
useEffect(() => {
const item = new Polyline([], {
color,
stroke: true,
opacity: 0.4,
weight: 9,
}).addTo(MainMap);
setLayer(item);
return () => MainMap.removeLayer(item);
}, [MainMap]);
useEffect(() => {
if (!layer) return;
layer.setLatLngs(latlngs);
layer.options.color = color;
}, [latlngs, layer, color]);
return null;
};
export { GpxPolyline };

View file

@ -12,11 +12,13 @@ import { TileLayer } from '~/map/TileLayer';
import { Stickers } from '~/map/Stickers';
import { KmMarks } from '~/map/KmMarks';
import { CurrentLocation } from '~/map/CurrentLocation';
import { GpxPolyline } from '~/map/GpxPolyline';
import 'leaflet/dist/leaflet.css';
import { selectEditorEditing, selectEditorMode } from '~/redux/editor/selectors';
import { selectEditorEditing, selectEditorMode, selectEditorGpx } from '~/redux/editor/selectors';
import { MODES } from '~/constants/modes';
import { selectUserLocation } from '~/redux/user/selectors';
import { GPX_ROUTE_COLORS } from '~/redux/editor/constants';
const mapStateToProps = state => ({
provider: selectMapProvider(state),
@ -25,6 +27,7 @@ const mapStateToProps = state => ({
editing: selectEditorEditing(state),
mode: selectEditorMode(state),
location: selectUserLocation(state),
gpx: selectEditorGpx(state),
});
const mapDispatchToProps = {
@ -44,6 +47,7 @@ const MapUnconnected: React.FC<IProps> = ({
editing,
mode,
location,
gpx,
mapClicked,
mapSetSticker,
@ -51,11 +55,7 @@ const MapUnconnected: React.FC<IProps> = ({
}) => {
const onClick = React.useCallback(
event => {
if (
!MainMap.clickable ||
mode === MODES.NONE
)
return;
if (!MainMap.clickable || mode === MODES.NONE) return;
mapClicked(event.latlng);
},
@ -86,6 +86,18 @@ const MapUnconnected: React.FC<IProps> = ({
<KmMarks />
<CurrentLocation location={location} />
{gpx.enabled &&
gpx.list.map(
({ latlngs, enabled, color }, index) =>
enabled && (
<GpxPolyline
latlngs={latlngs}
color={color}
key={index}
/>
)
)}
</div>,
document.getElementById('canvas')
);

View file

@ -151,3 +151,18 @@ export const editorSetDirection = (drawing_direction: IEditorState['drawing_dire
type: EDITOR_ACTIONS.SET_DIRECTION,
drawing_direction,
});
export const editorSetGpx = (gpx: Partial<IEditorState['gpx']>) => ({
type: EDITOR_ACTIONS.SET_GPX,
gpx,
});
export const editorUploadGpx = (file: File) => ({
type: EDITOR_ACTIONS.UPLOAD_GPX,
file,
});
export const editorDropGpx = (index: number) => ({
type: EDITOR_ACTIONS.DROP_GPX,
index,
});

View file

@ -1,5 +1,12 @@
const P = 'EDITOR';
export const GPX_ROUTE_COLORS = [
'#ff3344',
'#3344ff',
'#33ff44',
'#33ffff',
];
export const DRAWING_DIRECTIONS: Record<'FORWARDS' | 'BACKWARDS', 'forward' | 'backward'> = {
FORWARDS: 'forward',
BACKWARDS: 'backward',
@ -61,4 +68,7 @@ export const EDITOR_ACTIONS = {
CAPTURE_HIPSTORY: `${P}-CAPTURE_HIPSTORY`,
SET_DIRECTION: `${P}-SET_DIRECTION`,
SET_GPX: `${P}-SET_GPX`,
UPLOAD_GPX: `${P}-UPLOAD_GPX`,
DROP_GPX: `${P}-DROP_GPX`,
};

View file

@ -187,6 +187,17 @@ const setDirection = (
drawing_direction,
});
const setGpx = (
state,
{ gpx }: ReturnType<typeof ACTIONS.editorSetGpx>
): IEditorState => ({
...state,
gpx: {
...state.gpx,
...gpx,
},
});
export const EDITOR_HANDLERS = {
[EDITOR_ACTIONS.SET_EDITING]: setEditing,
[EDITOR_ACTIONS.SET_CHANGED]: setChanged,
@ -219,4 +230,5 @@ export const EDITOR_HANDLERS = {
[EDITOR_ACTIONS.SET_HISTORY]: setHistory,
[EDITOR_ACTIONS.SET_DIRECTION]: setDirection,
[EDITOR_ACTIONS.SET_GPX]: setGpx,
};

View file

@ -2,10 +2,19 @@ import { createReducer } from '~/utils/reducer';
import { DIALOGS } from '~/constants/dialogs';
import { MODES } from '~/constants/modes';
import { EDITOR_HANDLERS } from './handlers';
import { ILatLng } from '../map/types';
// import { ILatLng } from '../map/types';
import { INominatimResult } from '~/redux/types';
import { IMapReducer } from '../map';
import { DRAWING_DIRECTIONS } from './constants';
import { LatLng } from 'leaflet';
export interface IGpxRoute {
latlngs: LatLng[],
enabled: boolean;
color: string;
name: string;
id: string;
}
export interface IEditorHistoryItem {
route: IMapReducer['route'];
@ -19,8 +28,8 @@ export interface IEditorState {
markers_shown: boolean;
router: {
points: ILatLng[];
waypoints: ILatLng[];
points: LatLng[];
waypoints: LatLng[];
};
mode: typeof MODES[keyof typeof MODES];
@ -70,6 +79,12 @@ export interface IEditorState {
records: IEditorHistoryItem[];
position: number;
};
gpx: {
enabled: boolean;
list: IGpxRoute[],
},
}
export const EDITOR_INITIAL_STATE = {
@ -130,6 +145,11 @@ export const EDITOR_INITIAL_STATE = {
records: [{ route: [], stickers: [] }],
position: 0,
},
gpx: {
enabled: true,
list: [],
},
};
export const editor = createReducer(EDITOR_INITIAL_STATE, EDITOR_HANDLERS);

View file

@ -8,7 +8,7 @@ import {
takeLeading,
delay,
} from 'redux-saga/effects';
import { selectEditor, selectEditorMode } from '~/redux/editor/selectors';
import { selectEditor, selectEditorMode, selectEditorGpx } from '~/redux/editor/selectors';
import { simplify } from '~/utils/simplify';
import {
editorHideRenderer,
@ -30,6 +30,8 @@ import {
editorSetHistory,
editorUndo,
editorRedo,
editorUploadGpx,
editorSetGpx,
} from '~/redux/editor/actions';
import { getUrlData, pushPath } from '~/utils/history';
import { MODES } from '~/constants/modes';
@ -38,7 +40,7 @@ import { LatLng } from 'leaflet';
import { searchSetTab } from '../user/actions';
import { TABS, DIALOGS } from '~/constants/dialogs';
import { EDITOR_ACTIONS, EDITOR_HISTORY_LENGTH } from './constants';
import { getGPXString, downloadGPXTrack } from '~/utils/gpx';
import { getGPXString, downloadGPXTrack, importGpxTrack } from '~/utils/gpx';
import {
getTilePlacement,
getPolyPlacement,
@ -62,6 +64,9 @@ import { OsrmRouter } from '~/utils/map/OsrmRouter';
import path from 'ramda/es/path';
import { MainMap } from '~/constants/map';
import { EDITOR_INITIAL_STATE } from '.';
import { Unwrap } from '~/utils/middleware';
import uuid from 'uuid';
import { getRandomColor } from '~/utils/dom';
const hideLoader = () => {
document.getElementById('loader').style.opacity = String(0);
@ -240,9 +245,9 @@ function* keyPressedSaga({ key, target }: ReturnType<typeof editorKeyPressed>) {
yield put(editorChangeMode(MODES.TRASH));
}
} else if (key === 'z') {
yield put(editorUndo())
yield put(editorUndo());
} else if (key === 'x') {
yield put(editorRedo())
yield put(editorRedo());
}
}
@ -368,6 +373,29 @@ function* redoHistory() {
yield put(editorSetHistory({ position: history.position + 1 }));
}
function* uploadGpx({ file }: ReturnType<typeof editorUploadGpx>) {
const gpx: Unwrap<typeof importGpxTrack> = yield importGpxTrack(file);
if (!gpx || !gpx.length) return;
const { list }: ReturnType<typeof selectEditorGpx> = yield select(selectEditorGpx);
yield put(
editorSetGpx({
list: [
...list,
...gpx.map(item => ({
enabled: true,
latlngs: item.latlngs,
color: getRandomColor(),
name: item.name || `Track #${list.length + 1}`,
id: uuid() as string,
})),
],
})
);
}
export function* editorSaga() {
yield takeEvery(EDITOR_ACTIONS.LOCATION_CHANGED, locationChangeSaga);
@ -397,4 +425,6 @@ export function* editorSaga() {
yield takeEvery(EDITOR_ACTIONS.UNDO, undoHistory);
yield takeEvery(EDITOR_ACTIONS.REDO, redoHistory);
yield takeEvery(EDITOR_ACTIONS.UPLOAD_GPX, uploadGpx);
}

View file

@ -10,3 +10,4 @@ export const selectEditorRouter = (state: IState) => state.editor.router;
export const selectEditorDistance = (state: IState) => state.editor.distance;
export const selectEditorNominatim = (state: IState) => state.editor.nominatim;
export const selectEditorDirection = (state: IState) => state.editor.drawing_direction;
export const selectEditorGpx = (state: IState) => state.editor.gpx;

View file

@ -1,7 +1,8 @@
import { MAP_ACTIONS } from './constants';
import { IMapReducer } from './';
import { IStickerDump } from '~/redux/map/types';
import { ILatLng } from './types';
import { LatLng } from 'leaflet';
// import { ILatLng } from './types';
export const mapSet = (map: Partial<IMapReducer>) => ({
type: MAP_ACTIONS.SET_MAP,
@ -39,7 +40,7 @@ export const mapDropSticker = (index: number) => ({
index,
});
export const mapClicked = (latlng: ILatLng) => ({
export const mapClicked = (latlng: LatLng) => ({
type: MAP_ACTIONS.MAP_CLICKED,
latlng,
});

View file

@ -5,3 +5,5 @@ export const selectMapProvider = (state: IState) => state.map.provider;
export const selectMapLogo = (state: IState) => state.map.logo;
export const selectMapRoute= (state: IState) => state.map.route;
export const selectMapStickers = (state: IState) => state.map.stickers;
export const selectMapTitle= (state: IState) => state.map.title;
export const selectMapAddress = (state: IState) => state.map.address;

View file

@ -1,15 +1,9 @@
import { LatLng } from 'leaflet';
import { IRoutePoint } from '~/utils/gpx';
export type ILatLng = {
lat: number;
lng: number;
};
export type IMapRoute = ILatLng[];
export type IMapRoute = LatLng[];
export interface IStickerDump {
latlng: ILatLng;
latlng: LatLng;
set: string;
sticker: string;
angle?: number;
@ -21,7 +15,7 @@ export interface IRoute {
title: string;
owner: number;
address: string;
route: IRoutePoint[];
route: LatLng[];
stickers: IStickerDump[];
provider: string;
is_public: boolean;

View file

@ -26,6 +26,12 @@ const userPersistConfig: PersistConfig = {
storage,
};
const editorPersistConfig: PersistConfig = {
key: 'editor',
whitelist: ['gpx'],
storage,
};
export interface IState {
user: IRootReducer;
map: IMapReducer;
@ -43,8 +49,8 @@ const composeEnhancers =
export const store = createStore(
combineReducers({
user: persistReducer(userPersistConfig, userReducer),
editor: persistReducer(editorPersistConfig, editor),
map,
editor,
}),
composeEnhancers(applyMiddleware(sagaMiddleware))
);

View file

@ -343,7 +343,7 @@ export function* updateUserRoutes() {
}
export function* userSaga() {
yield takeLatest(REHYDRATE, authCheckSaga);
yield takeEvery(REHYDRATE, authCheckSaga);
yield takeEvery(USER_ACTIONS.USER_LOGOUT, userLogoutSaga);
yield takeLatest(USER_ACTIONS.GOT_VK_USER, gotVkUserSaga);

View file

@ -48,11 +48,11 @@
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="22.770291"
inkscape:cy="17.290238"
inkscape:zoom="5.6568542"
inkscape:cx="7.5044641"
inkscape:cy="20.369143"
inkscape:document-units="px"
inkscape:current-layer="g2550"
inkscape:current-layer="svg8"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1015"
@ -61,7 +61,7 @@
inkscape:window-maximized="1"
units="px"
inkscape:showpageshadow="false"
inkscape:snap-global="false" />
inkscape:snap-global="true" />
<metadata
id="metadata5">
<rdf:RDF>
@ -693,18 +693,78 @@
<g
id="g2550"
transform="translate(0.9697265,-0.5)"
style="fill:#ffffff" />
<g
style="fill:#1a1a1a"
id="g1002"
transform="translate(-384)">
<rect
style="opacity:1;fill:#1a1a1a;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.94117647"
id="rect1000"
width="32"
height="32"
x="0"
y="0" />
</g>
<g
style="fill:#ffffff"
transform="translate(-383.03028,-0.5)"
id="g1008">
<path
sodipodi:nodetypes="cccccc"
inkscape:connector-curvature="0"
id="path1004"
d="m 3.125,14.715588 h 11.548483 l 2.025957,1.681298 -2.025957,1.887526 H 3.125 Z"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.25750196;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccccccccccccc"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.12910175;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 19.080155,11.622728 -2.182066,2.182066 2.695206,2.695206 -2.695206,2.695205 2.182066,2.182066 2.695205,-2.695206 2.695206,2.695206 2.182066,-2.182066 L 23.957426,16.5 26.652632,13.804794 24.470566,11.622728 21.77536,14.317934 Z"
id="path1006"
inkscape:connector-curvature="0" />
</g>
<g
style="fill:#1a1a1a"
id="g1057"
transform="translate(-416)">
<rect
style="opacity:1;fill:#1a1a1a;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.94117647"
id="rect1055"
width="32"
height="32"
x="0"
y="0" />
</g>
<path
sodipodi:nodetypes="cccccccccccc"
inkscape:connector-curvature="0"
id="path1059"
d="m -398.03987,8.959327 c 0.96726,6.15e-5 2.83931,0.419056 2.83931,0.419056 l 0.81565,-1.6970847 2.25681,4.8545537 -5.09396,1.335474 0.77693,-1.937748 c 0,0 -1.10109,-0.113536 -1.59474,-0.114876 -3.85069,4.73e-4 -6.97217,3.12196 -6.97265,6.972656 10e-4,1.212906 0.31917,2.404467 0.92188,3.457031 l -2.07031,2.070313 c -1.11178,-1.629145 -1.70791,-3.554996 -1.71094,-5.527344 2.9e-4,-5.429961 4.40206,-9.8317428 9.83202,-9.832031 z"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.13512897;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<g
id="g1083"
transform="rotate(-180,16.23592,15.944757)">
<g
style="fill:#ffffff"
transform="matrix(1.1301151,0,0,1.1301151,-1.1473427,-2.6397119)"
id="g1065">
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path1061"
d="m 21.578609,12.373297 4.755161,4.023589 -4.755161,4.119332 c 0,-2.714307 0,-5.428614 0,-8.142921 z"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.25750196;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<g
id="g1073"
transform="translate(-6.0736195,-0.5)"
style="fill:#ffffff">
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.0570724;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 5,15 h 9.707804 L 16.41085,16.413321 14.707804,18 H 5 Z"
id="rect2536"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.93824887;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 9.9326117,14.715588 H 29.84292 v 3.568824 H 9.9326117 Z"
id="path1075"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
inkscape:connector-curvature="0"
id="rect2540"
d="m 18.41211,12.400102 -1.834273,1.834273 2.265625,2.265625 -2.265625,2.265624 1.834273,1.834273 2.265624,-2.265625 2.265625,2.265625 1.834273,-1.834273 L 22.512007,16.5 l 2.265625,-2.265625 -1.834273,-1.834273 -2.265625,2.265625 z"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.94913757;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:nodetypes="ccccccccccccc" />
sodipodi:nodetypes="ccccc" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before After
Before After

View file

@ -14,6 +14,12 @@
color: white;
font-weight: 400;
border: none;
position: relative;
&.outline {
box-shadow: inset #444444 0 0 0 1px;
background: transparent;
}
&.primary {
background: #3c78db;
@ -29,6 +35,21 @@
background: #17bf6d;
color: white;
}
&.disabled {
opacity: 0.5;
touch-action: none;
pointer-events: none;
}
input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
}
}
.button-group {

81
src/styles/gpx.less Normal file
View file

@ -0,0 +1,81 @@
.gpx-title {
display: flex;
flex-direction: row;
padding: 10px;
}
.gpx-row {
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: row;
padding: 10px;
min-width: 0;
&_disabled {
opacity: 0.5;
}
&__title {
flex: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
margin: 0 10px;
cursor: pointer;
}
&__buttons {
display: flex;
align-items: center;
justify-content: center;
svg {
fill: white;
}
> div {
padding: 0 5px;
cursor: pointer;
&:last-child {
padding-right: 0;
}
&:first-child {
padding-left: 0;
}
}
}
&__color {
width: 8px;
height: 8px;
border-radius: 100%;
}
}
.gpx-buttons {
padding: 10px;
& > * {
margin: 5px 0;
&:last-child {
margin-bottom: 0;
}
&:first-child {
margin-top: 0;
}
}
& > .button {
width: 100%;
box-sizing: border-box;
position: relative;
height: 32px;
align-items: center;
justify-content: center;
}
}

View file

@ -13,6 +13,7 @@
@import 'progress.less';
@import 'slider.less';
@import 'switch.less';
@import 'gpx.less';
body {
font-family: 'Rubik', sans-serif;

View file

@ -290,47 +290,11 @@
top: 52px;
}
&.left {
&.left, &__left {
right: auto;
left: 10px;
}
// &.control-dialog-big {
// min-width: 555px;
// @media (max-width: @mobile_breakpoint) {
// min-width: 100%;
// max-width: 100%;
// width: 100% !important;
// left: 0;
// padding: 0 10px;
// }
// }
// &.control-dialog-medium {
// min-width: 460px;
// @media (max-width: @mobile_breakpoint) {
// min-width: 100%;
// max-width: 100%;
// width: 100% !important;
// left: 0;
// padding: 0 10px;
// }
// }
// &.control-dialog-small {
// min-width: 270px;
// @media (max-width: @mobile_breakpoint) {
// min-width: 100%;
// max-width: 100%;
// width: 100% !important;
// left: 0;
// padding: 0 10px;
// }
// }
.helper:first-child {
border-radius: @panel_radius @panel_radius 0 0;
}
@ -352,6 +316,11 @@
width: calc(100% - 20px);
max-width: 417px;
}
&__small {
width: calc(100% - 20px);
max-width: 324px;
}
}
.control-dialog-provider {

View file

@ -146,6 +146,10 @@
padding: 0 10px 2px 10px;
align-items: center;
display: flex;
.switch {
margin-right: 0.5em;
}
}
.save-description {

View file

@ -7,7 +7,6 @@
vertical-align: text-top;
position: relative;
top: 0.05em;
margin-right: 0.5em;
transition: all 500ms;
&::after {

View file

@ -7,3 +7,8 @@ export const getStyle = (oElm: any, strCssRule: string): string => {
return '';
};
export const getRandomColor = () => {
return `hsl(${(Math.floor(Math.random() * 360))}, 100%, 50%)`
}

View file

@ -1,11 +1,11 @@
import { LatLng, LatLngLiteral, point, Point, PointExpression } from 'leaflet';
import { LatLng, LatLngLiteral, point, Point, PointExpression, latLng } from 'leaflet';
interface ILatLng {
lat: number;
lng: number;
}
// interface LatLng {
// lat: number;
// lng: number;
// }
export const middleCoord = (l1: ILatLng, l2: ILatLng): ILatLng => ({
export const middleCoord = (l1: LatLng, l2: LatLng): LatLng => latLng({
lat: l2.lat + (l1.lat - l2.lat) / 2,
lng: l2.lng + (l1.lng - l2.lng) / 2,
});

View file

@ -1,52 +1,110 @@
import saveAs from 'file-saver';
import GPX from 'gpx-parser-builder';
import { LatLng } from 'leaflet';
export interface IRoutePoint {
lat: number,
lng: number,
}
type GpxImportTrkPt = {
$: { lat: string; lon: string }[];
name: string;
};
type GpxImportTrkSeg = {
trkpt: { trkpt: GpxImportTrkPt }[];
};
type GpxImportRaw = {
metadata: { name: string };
trk: {
name: string;
trkseg: GpxImportTrkSeg[];
}[];
};
// export interface IRoutePoint {
// lat: number;
// lng: number;
// }
interface IGPXSticker {
latlng: IRoutePoint,
text?: string,
latlng: LatLng;
text?: string;
}
interface IGetGPXString {
route: Array<IRoutePoint>,
stickers?: Array<IGPXSticker>
title?: string,
route: Array<LatLng>;
stickers?: Array<IGPXSticker>;
title?: string;
}
export const getGPXString = ({ route, title, stickers }: IGetGPXString): string => (`<?xml version="1.0" encoding="UTF-8"?>
export const getGPXString = ({
route,
title,
stickers,
}: IGetGPXString): string => `<?xml version="1.0" encoding="UTF-8"?>
<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<metadata>
<name>${title || 'GPX Track'}</name>
</metadata>
${
stickers.reduce((cat, { latlng: { lat, lng }, text }) => (
${stickers.reduce(
(cat, { latlng: { lat, lng }, text }) =>
`${cat}
<wpt lat="${lat}" lon="${lng}">
<name>${text}</name>
<sym>generic</sym>
<type>${title}</type>
</wpt>`), '')
}
</wpt>`,
''
)}
<trk>
<name>${title || 'GPX Track'}</name>
<trkseg>
${
route.reduce((cat, { lat, lng }) => (
${route.reduce(
(cat, { lat, lng }) =>
` ${cat}
<trkpt lat="${lat}" lon="${lng}" />`
), '')
}
<trkpt lat="${lat}" lon="${lng}" />`,
''
)}
</trkseg>
</trk>
</gpx>
`);
`;
export const downloadGPXTrack = ({ track, title }: { track: string, title?: string }): void => (
export const downloadGPXTrack = ({ track, title }: { track: string; title?: string }): void =>
saveAs(
new Blob([track], { type: 'application/gpx+xml;charset=utf-8' }),
`${(title || 'track').replace(/\./ig, ' ')}.gpx`
)
`${(title || 'track').replace(/\./gi, ' ')}.gpx`
);
export const importGpxTrack = async (file: File) => {
const reader = new FileReader();
const content = await new Promise(resolve => {
reader.readAsText(file);
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = () => {
resolve(null);
};
});
const gpx: GpxImportRaw = GPX.parse(content);
console.log(gpx);
if (!gpx || !gpx.trk) return null;
const latlngs: LatLng[] = gpx.trk.reduce((trk_res, trk) => {
return trk.trkseg.reduce((trkseg_res, trkseg) => {
return [
...trkseg_res,
...trkseg.trkpt.map(pnt => ({ lat: pnt['$'].lat, lng: pnt['$'].lon })),
];
}, trk_res);
}, []);
return [
{
name: gpx.metadata.name || '',
latlngs,
},
];
};

View file

@ -3,7 +3,7 @@ import { COLORS, CLIENT } from '~/config/frontend';
import saveAs from 'file-saver';
import { replaceProviderUrl } from '~/constants/providers';
import { STICKERS } from '~/constants/stickers';
import { ILatLng, IRoute } from '~/redux/map/types';
import { IRoute } from '~/redux/map/types';
import { IStickerDump } from '~/redux/map/types';
import { IRootState } from '~/redux/user';
import {
@ -12,7 +12,7 @@ import {
findDistancePx,
middleCoordPx,
} from '~/utils/geom';
import { Point } from 'leaflet';
import { Point, LatLng, latLng } from 'leaflet';
import { MainMap } from '~/constants/map';
export interface ITilePlacement {
@ -55,14 +55,14 @@ const latLngToTile = (latlng: {
return { x: xtile, y: ytile, z: zoom };
};
const tileToLatLng = (point: { x: number; y: number }): ILatLng => {
const tileToLatLng = (point: { x: number; y: number }): LatLng => {
const map = MainMap;
const z = map.getZoom();
const lng = (point.x / Math.pow(2, z)) * 360 - 180;
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)));
return { lat, lng };
return latLng({ lat, lng });
};
export const getTilePlacement = (): ITilePlacement => {
@ -102,7 +102,7 @@ export const getTilePlacement = (): ITilePlacement => {
};
};
export const getPolyPlacement = (latlngs: ILatLng[]): Point[] =>
export const getPolyPlacement = (latlngs: LatLng[]): Point[] =>
latlngs.length === 0
? []
: latlngs.map(latlng => (MainMap.latLngToContainerPoint(latlng)));

View file

@ -1,7 +1,7 @@
import { Map, LineUtil } from 'leaflet';
import { ILatLng } from "~/redux/map/types";
import { Map, LineUtil, LatLng } from 'leaflet';
// import { ILatLng } from "~/redux/map/types";
export const simplify = ({ map, latlngs }: { map: Map, latlngs: ILatLng[] }): ILatLng[] => {
export const simplify = ({ map, latlngs }: { map: Map, latlngs: LatLng[] }): LatLng[] => {
const zoom = 12;
const mul = 0.7; // 0 - not simplifying, 1 - very rude.
const points = latlngs.map(({ lat, lng }) => map.project({ lat, lng }, zoom));

11004
yarn.lock Normal file

File diff suppressed because it is too large Load diff