mirror of
https://github.com/muerwre/orchidmap-front.git
synced 2025-04-25 02:56:41 +07:00
added gpx dialog
This commit is contained in:
parent
947ec69e60
commit
e995b46641
33 changed files with 11687 additions and 131 deletions
32
package-lock.json
generated
32
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
147
src/components/dialogs/GpxDialog.tsx
Normal file
147
src/components/dialogs/GpxDialog.tsx
Normal 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 };
|
34
src/components/gpx/GpxDialogRow.tsx
Normal file
34
src/components/gpx/GpxDialogRow.tsx
Normal 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 };
|
|
@ -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) =>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -12,4 +12,5 @@ export const MODES = {
|
|||
CONFIRM_CANCEL: 'CONFIRM_CANCEL',
|
||||
PROVIDER: 'PROVIDER',
|
||||
SHOT_PREFETCH: 'SHOT_PREFETCH',
|
||||
GPX: 'GPX',
|
||||
};
|
||||
|
|
35
src/map/GpxPolyline/index.tsx
Normal file
35
src/map/GpxPolyline/index.tsx
Normal 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 };
|
|
@ -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')
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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`,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
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
|
||||
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"
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
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
|
||||
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="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: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="ccccc" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
@ -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
81
src/styles/gpx.less
Normal 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;
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
@import 'progress.less';
|
||||
@import 'slider.less';
|
||||
@import 'switch.less';
|
||||
@import 'gpx.less';
|
||||
|
||||
body {
|
||||
font-family: 'Rubik', sans-serif;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -146,6 +146,10 @@
|
|||
padding: 0 10px 2px 10px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
.switch {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.save-description {
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
vertical-align: text-top;
|
||||
position: relative;
|
||||
top: 0.05em;
|
||||
margin-right: 0.5em;
|
||||
transition: all 500ms;
|
||||
|
||||
&::after {
|
||||
|
|
|
@ -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%)`
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
|
|
110
src/utils/gpx.ts
110
src/utils/gpx.ts
|
@ -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 }) => (
|
||||
`${cat}
|
||||
${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 }) => (
|
||||
` ${cat}
|
||||
<trkpt lat="${lat}" lon="${lng}" />`
|
||||
), '')
|
||||
}
|
||||
${route.reduce(
|
||||
(cat, { lat, lng }) =>
|
||||
` ${cat}
|
||||
<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,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue