mirror of
https://github.com/muerwre/orchidmap-front.git
synced 2025-04-24 18:46:40 +07:00
separated map and user reducers
This commit is contained in:
parent
9f8cb1a875
commit
b75c028ce1
14 changed files with 849 additions and 768 deletions
5
.prettierrc
Normal file
5
.prettierrc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
|
@ -1,78 +1,81 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { marker } from 'leaflet';
|
// import { marker } from 'leaflet';
|
||||||
import { DomMarker } from '$utils/DomMarker';
|
// import { DomMarker } from '$utils/DomMarker';
|
||||||
import { Icon } from '$components/panels/Icon';
|
// import { Icon } from '$components/panels/Icon';
|
||||||
import { editor } from '$modules/Editor';
|
// import { editor } from '$modules/Editor';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserLocation extends React.Component<Props, {}> {
|
export class UserLocation extends React.Component<Props, {}> {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const element = document.createElement('div');
|
|
||||||
this.icon = new DomMarker({ element, className: 'location-marker' });
|
|
||||||
|
|
||||||
this.map = editor.map.map;
|
|
||||||
this.location = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
icon;
|
|
||||||
mark = null;
|
|
||||||
map;
|
|
||||||
location;
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.getUserLocation(this.updateLocationMark);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserLocation = callback => {
|
|
||||||
// todo: TO SAGAS
|
|
||||||
if (!window.navigator || !window.navigator.geolocation) return;
|
|
||||||
|
|
||||||
window.navigator.geolocation.getCurrentPosition(position => {
|
|
||||||
if (!position || !position.coords || !position.coords.latitude || !position.coords.longitude) return;
|
|
||||||
|
|
||||||
const { latitude, longitude } = position.coords;
|
|
||||||
|
|
||||||
callback(latitude, longitude);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
centerMapOnLocation = () => {
|
|
||||||
if (this.location && this.location.length === 2) {
|
|
||||||
this.panMapTo(this.location[0], this.location[1]);
|
|
||||||
} else {
|
|
||||||
this.getUserLocation(this.panMapTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getUserLocation(this.updateLocationMark);
|
|
||||||
};
|
|
||||||
|
|
||||||
panMapTo = (latitude, longitude) => {
|
|
||||||
if (!latitude || !longitude) return;
|
|
||||||
|
|
||||||
this.map.panTo([latitude, longitude]);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateLocationMark = (latitude, longitude) => {
|
|
||||||
if (!latitude || !longitude) return;
|
|
||||||
|
|
||||||
if (this.mark) this.map.removeLayer(this.mark);
|
|
||||||
|
|
||||||
this.location = [latitude, longitude];
|
|
||||||
this.mark = marker(this.location, { icon: this.icon }).addTo(this.map);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return null
|
||||||
<div className="status-bar square pointer pointer">
|
|
||||||
<div onClick={this.centerMapOnLocation}>
|
|
||||||
<Icon icon="icon-locate" size={30} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
// constructor(props) {
|
||||||
|
// super(props);
|
||||||
|
|
||||||
|
// const element = document.createElement('div');
|
||||||
|
// this.icon = new DomMarker({ element, className: 'location-marker' });
|
||||||
|
|
||||||
|
// // this.map = editor.map.map;
|
||||||
|
// this.location = [];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// icon;
|
||||||
|
// mark = null;
|
||||||
|
// map;
|
||||||
|
// location;
|
||||||
|
|
||||||
|
// componentDidMount() {
|
||||||
|
// this.getUserLocation(this.updateLocationMark);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// getUserLocation = callback => {
|
||||||
|
// // todo: TO SAGAS
|
||||||
|
// if (!window.navigator || !window.navigator.geolocation) return;
|
||||||
|
|
||||||
|
// window.navigator.geolocation.getCurrentPosition(position => {
|
||||||
|
// if (!position || !position.coords || !position.coords.latitude || !position.coords.longitude) return;
|
||||||
|
|
||||||
|
// const { latitude, longitude } = position.coords;
|
||||||
|
|
||||||
|
// callback(latitude, longitude);
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// centerMapOnLocation = () => {
|
||||||
|
// if (this.location && this.location.length === 2) {
|
||||||
|
// this.panMapTo(this.location[0], this.location[1]);
|
||||||
|
// } else {
|
||||||
|
// this.getUserLocation(this.panMapTo);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.getUserLocation(this.updateLocationMark);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// panMapTo = (latitude, longitude) => {
|
||||||
|
// if (!latitude || !longitude) return;
|
||||||
|
|
||||||
|
// this.map.panTo([latitude, longitude]);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// updateLocationMark = (latitude, longitude) => {
|
||||||
|
// if (!latitude || !longitude) return;
|
||||||
|
|
||||||
|
// if (this.mark) this.map.removeLayer(this.mark);
|
||||||
|
|
||||||
|
// this.location = [latitude, longitude];
|
||||||
|
// this.mark = marker(this.location, { icon: this.icon }).addTo(this.map);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// render() {
|
||||||
|
// return (
|
||||||
|
// <div className="status-bar square pointer pointer">
|
||||||
|
// <div onClick={this.centerMapOnLocation}>
|
||||||
|
// <Icon icon="icon-locate" size={30} />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { TileLayer } from "$containers/map/TileLayer";
|
||||||
import { Stickers } from "$containers/map/Stickers";
|
import { Stickers } from "$containers/map/Stickers";
|
||||||
import { selectUserEditing } from '$redux/user/selectors'
|
import { selectUserEditing } from '$redux/user/selectors'
|
||||||
|
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
provider: selectMapProvider(state),
|
provider: selectMapProvider(state),
|
||||||
route: selectMapRoute(state),
|
route: selectMapRoute(state),
|
||||||
|
@ -32,6 +34,8 @@ type IProps = React.HTMLAttributes<HTMLDivElement> &
|
||||||
ReturnType<typeof mapStateToProps> &
|
ReturnType<typeof mapStateToProps> &
|
||||||
typeof mapDispatchToProps & {};
|
typeof mapDispatchToProps & {};
|
||||||
|
|
||||||
|
export let MainMap = map(document.getElementById('canvas')).setView([55.0153275, 82.9071235], 13);
|
||||||
|
|
||||||
const MapUnconnected: React.FC<IProps> = ({
|
const MapUnconnected: React.FC<IProps> = ({
|
||||||
provider,
|
provider,
|
||||||
route,
|
route,
|
||||||
|
@ -53,8 +57,8 @@ const MapUnconnected: React.FC<IProps> = ({
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
|
||||||
setLayer(map(ref.current).setView([55.0153275, 82.9071235], 13));
|
setLayer(MainMap);
|
||||||
}, [ref]);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!layer) return;
|
if (!layer) return;
|
||||||
|
|
|
@ -131,8 +131,7 @@
|
||||||
<body>
|
<body>
|
||||||
<canvas id="renderer"></canvas>
|
<canvas id="renderer"></canvas>
|
||||||
|
|
||||||
<section id="map" style="position: absolute; width: 100%; height: 100%;"></section>
|
<section id="canvas" style="position: absolute; width: 100%; height: 100%;"></section>
|
||||||
<section id="canvas" style="position: absolute; width: 50%; height: 100%;right: 0;"></section>
|
|
||||||
|
|
||||||
<section id="loader">
|
<section id="loader">
|
||||||
<div id="loader-container">
|
<div id="loader-container">
|
||||||
|
|
|
@ -24,6 +24,11 @@ export const mapSetSticker = (index: number, sticker: IStickerDump) => ({
|
||||||
sticker,
|
sticker,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mapAddSticker = (sticker: IStickerDump) => ({
|
||||||
|
type: MAP_ACTIONS.ADD_STICKER,
|
||||||
|
sticker,
|
||||||
|
});
|
||||||
|
|
||||||
export const mapDropSticker = (index: number) => ({
|
export const mapDropSticker = (index: number) => ({
|
||||||
type: MAP_ACTIONS.DROP_STICKER,
|
type: MAP_ACTIONS.DROP_STICKER,
|
||||||
index,
|
index,
|
||||||
|
@ -33,3 +38,28 @@ export const mapClicked = (latlng: ILatLng) => ({
|
||||||
type: MAP_ACTIONS.MAP_CLICKED,
|
type: MAP_ACTIONS.MAP_CLICKED,
|
||||||
latlng,
|
latlng,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mapSetTitle = (title: string) => ({
|
||||||
|
type: MAP_ACTIONS.SET_TITLE,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mapSetDescription = (description: string) => ({
|
||||||
|
type: MAP_ACTIONS.SET_DESCRIPTION,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mapSetAddress = (address: string) => ({
|
||||||
|
type: MAP_ACTIONS.SET_ADDRESS,
|
||||||
|
address,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mapSetOwner = (owner: IMapReducer['owner']) => ({
|
||||||
|
type: MAP_ACTIONS.SET_DESCRIPTION,
|
||||||
|
owner,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mapSetPublic = (is_public: IMapReducer['is_public']) => ({
|
||||||
|
type: MAP_ACTIONS.SET_PUBLIC,
|
||||||
|
is_public,
|
||||||
|
});
|
||||||
|
|
|
@ -4,7 +4,13 @@ export const MAP_ACTIONS = {
|
||||||
SET_MAP: `${P}-SET_MAP`,
|
SET_MAP: `${P}-SET_MAP`,
|
||||||
SET_PROVIDER: `${P}-SET_PROVIDER`,
|
SET_PROVIDER: `${P}-SET_PROVIDER`,
|
||||||
SET_ROUTE: `${P}-SET_ROUTE`,
|
SET_ROUTE: `${P}-SET_ROUTE`,
|
||||||
|
SET_TITLE: `${P}-SET_TILE`,
|
||||||
|
SET_DESCRIPTION: `${P}-SETDESCRIPTION`,
|
||||||
|
SET_ADDRESS: `${P}-SET_ADDRESS`,
|
||||||
|
SET_OWNER: `${P}-SET_OWNER`,
|
||||||
|
SET_PUBLIC: `${P}-SET_PUBLIC`,
|
||||||
|
|
||||||
|
ADD_STICKER: `${P}-ADD_STICKER`,
|
||||||
SET_STICKER: `${P}-SET_STICKER`,
|
SET_STICKER: `${P}-SET_STICKER`,
|
||||||
DROP_STICKER: `${P}-DROP_STICKER`,
|
DROP_STICKER: `${P}-DROP_STICKER`,
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,21 @@
|
||||||
import { MAP_ACTIONS } from "./constants";
|
import { MAP_ACTIONS } from './constants';
|
||||||
import { IMapReducer } from ".";
|
import { IMapReducer } from '.';
|
||||||
import { mapSet, mapSetProvider, mapSetRoute, mapSetSticker } from "./actions";
|
import {
|
||||||
|
mapSet,
|
||||||
|
mapSetProvider,
|
||||||
|
mapSetRoute,
|
||||||
|
mapSetSticker,
|
||||||
|
mapAddSticker,
|
||||||
|
mapSetTitle,
|
||||||
|
mapSetAddress,
|
||||||
|
mapSetDescription,
|
||||||
|
mapSetOwner,
|
||||||
|
mapSetPublic,
|
||||||
|
} from './actions';
|
||||||
|
|
||||||
const setMap = (
|
const setMap = (state: IMapReducer, { map }: ReturnType<typeof mapSet>): IMapReducer => ({
|
||||||
state: IMapReducer,
|
|
||||||
{ map }: ReturnType<typeof mapSet>
|
|
||||||
): IMapReducer => ({
|
|
||||||
...state,
|
...state,
|
||||||
...map
|
...map,
|
||||||
});
|
});
|
||||||
|
|
||||||
const setProvider = (
|
const setProvider = (
|
||||||
|
@ -15,15 +23,12 @@ const setProvider = (
|
||||||
{ provider }: ReturnType<typeof mapSetProvider>
|
{ provider }: ReturnType<typeof mapSetProvider>
|
||||||
): IMapReducer => ({
|
): IMapReducer => ({
|
||||||
...state,
|
...state,
|
||||||
provider
|
provider,
|
||||||
});
|
});
|
||||||
|
|
||||||
const setRoute = (
|
const setRoute = (state: IMapReducer, { route }: ReturnType<typeof mapSetRoute>): IMapReducer => ({
|
||||||
state: IMapReducer,
|
|
||||||
{ route }: ReturnType<typeof mapSetRoute>
|
|
||||||
): IMapReducer => ({
|
|
||||||
...state,
|
...state,
|
||||||
route
|
route,
|
||||||
});
|
});
|
||||||
|
|
||||||
const setSticker = (
|
const setSticker = (
|
||||||
|
@ -31,7 +36,7 @@ const setSticker = (
|
||||||
{ sticker, index }: ReturnType<typeof mapSetSticker>
|
{ sticker, index }: ReturnType<typeof mapSetSticker>
|
||||||
): IMapReducer => ({
|
): IMapReducer => ({
|
||||||
...state,
|
...state,
|
||||||
stickers: state.stickers.map((item, i) => (i === index ? sticker : item))
|
stickers: state.stickers.map((item, i) => (i === index ? sticker : item)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropSticker = (
|
const dropSticker = (
|
||||||
|
@ -39,7 +44,40 @@ const dropSticker = (
|
||||||
{ index }: ReturnType<typeof mapSetSticker>
|
{ index }: ReturnType<typeof mapSetSticker>
|
||||||
): IMapReducer => ({
|
): IMapReducer => ({
|
||||||
...state,
|
...state,
|
||||||
stickers: state.stickers.filter((_, i) => i !== index)
|
stickers: state.stickers.filter((_, i) => i !== index),
|
||||||
|
});
|
||||||
|
|
||||||
|
const addSticker = (
|
||||||
|
state: IMapReducer,
|
||||||
|
{ sticker }: ReturnType<typeof mapAddSticker>
|
||||||
|
): IMapReducer => ({
|
||||||
|
...state,
|
||||||
|
stickers: [...state.stickers, sticker],
|
||||||
|
});
|
||||||
|
|
||||||
|
const setTitle = (state: IMapReducer, { title }: ReturnType<typeof mapSetTitle>): IMapReducer => ({
|
||||||
|
...state,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setAddress = (state: IMapReducer, { address }: ReturnType<typeof mapSetAddress>): IMapReducer => ({
|
||||||
|
...state,
|
||||||
|
address,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setDescription = (state: IMapReducer, { description }: ReturnType<typeof mapSetDescription>): IMapReducer => ({
|
||||||
|
...state,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setOwner = (state: IMapReducer, { owner }: ReturnType<typeof mapSetOwner>): IMapReducer => ({
|
||||||
|
...state,
|
||||||
|
owner,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setPublic = (state: IMapReducer, { is_public }: ReturnType<typeof mapSetPublic>): IMapReducer => ({
|
||||||
|
...state,
|
||||||
|
is_public,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MAP_HANDLERS = {
|
export const MAP_HANDLERS = {
|
||||||
|
@ -48,4 +86,10 @@ export const MAP_HANDLERS = {
|
||||||
[MAP_ACTIONS.SET_ROUTE]: setRoute,
|
[MAP_ACTIONS.SET_ROUTE]: setRoute,
|
||||||
[MAP_ACTIONS.SET_STICKER]: setSticker,
|
[MAP_ACTIONS.SET_STICKER]: setSticker,
|
||||||
[MAP_ACTIONS.DROP_STICKER]: dropSticker,
|
[MAP_ACTIONS.DROP_STICKER]: dropSticker,
|
||||||
|
[MAP_ACTIONS.ADD_STICKER]: addSticker,
|
||||||
|
[MAP_ACTIONS.SET_TITLE]: setTitle,
|
||||||
|
[MAP_ACTIONS.SET_ADDRESS]: setAddress,
|
||||||
|
[MAP_ACTIONS.SET_DESCRIPTION]: setDescription,
|
||||||
|
[MAP_ACTIONS.SET_OWNER]: setOwner,
|
||||||
|
[MAP_ACTIONS.SET_PUBLIC]: setPublic,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,13 +7,23 @@ import { IStickerDump } from '$modules/Sticker';
|
||||||
export interface IMapReducer {
|
export interface IMapReducer {
|
||||||
provider: string;
|
provider: string;
|
||||||
route: IMapRoute;
|
route: IMapRoute;
|
||||||
stickers: IStickerDump[]
|
stickers: IStickerDump[];
|
||||||
|
title: string;
|
||||||
|
address: string;
|
||||||
|
description: string;
|
||||||
|
owner: { id: string };
|
||||||
|
is_public: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MAP_INITIAL_STATE = {
|
export const MAP_INITIAL_STATE: IMapReducer = {
|
||||||
provider: DEFAULT_PROVIDER,
|
provider: DEFAULT_PROVIDER,
|
||||||
route: [],
|
route: [],
|
||||||
stickers: [],
|
stickers: [],
|
||||||
|
title: '',
|
||||||
|
address: '',
|
||||||
|
description: '',
|
||||||
|
owner: { id: null },
|
||||||
|
is_public: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const map = createReducer(MAP_INITIAL_STATE, MAP_HANDLERS)
|
export const map = createReducer(MAP_INITIAL_STATE, MAP_HANDLERS)
|
|
@ -1,40 +1,323 @@
|
||||||
import { takeEvery, select, put } from "redux-saga/effects";
|
import { takeEvery, select, put, call, TakeEffect, race, take, takeLatest } from 'redux-saga/effects';
|
||||||
import { MAP_ACTIONS } from "./constants";
|
import { MAP_ACTIONS } from './constants';
|
||||||
import { mapClicked, mapSet } from "./actions";
|
import { mapClicked, mapAddSticker, mapSetProvider, mapSet, mapSetTitle, mapSetAddress, mapSetDescription, mapSetOwner, mapSetPublic } from './actions';
|
||||||
import { selectUserMode, selectUserActiveSticker } from "$redux/user/selectors";
|
import { selectUserMode, selectUserActiveSticker, selectUser, selectUserUser } from '$redux/user/selectors';
|
||||||
import { IRootReducer } from "$redux/user";
|
import { MODES } from '$constants/modes';
|
||||||
import { MODES } from "$constants/modes";
|
import {
|
||||||
import { selectMapStickers } from "./selectors";
|
setMode,
|
||||||
import { setActiveSticker, setMode } from "$redux/user/actions";
|
setChanged,
|
||||||
|
setAddressOrigin,
|
||||||
|
setEditing,
|
||||||
|
setReady,
|
||||||
|
setActiveSticker,
|
||||||
|
setSaveError,
|
||||||
|
setSaveLoading,
|
||||||
|
sendSaveRequest,
|
||||||
|
setSaveSuccess,
|
||||||
|
setSaveOverwrite,
|
||||||
|
} from '$redux/user/actions';
|
||||||
|
import { pushLoaderState, getUrlData, pushPath, replacePath } from '$utils/history';
|
||||||
|
import { setReadySaga, searchSetSagaWorker } from '$redux/user/sagas';
|
||||||
|
import { getStoredMap, postMap } from '$utils/api';
|
||||||
|
import { Unwrap } from '$utils/middleware';
|
||||||
|
import { DEFAULT_PROVIDER } from '$constants/providers';
|
||||||
|
import { USER_ACTIONS } from '$redux/user/constants';
|
||||||
|
import { selectMap } from './selectors';
|
||||||
|
import { TIPS } from '$constants/tips';
|
||||||
|
import { delay } from 'redux-saga';
|
||||||
|
|
||||||
function* onMapClick({ latlng }: ReturnType<typeof mapClicked>) {
|
function* onMapClick({ latlng }: ReturnType<typeof mapClicked>) {
|
||||||
const mode = yield select(selectUserMode);
|
const mode = yield select(selectUserMode);
|
||||||
const { set, sticker } = yield select(selectUserActiveSticker);
|
const { set, sticker } = yield select(selectUserActiveSticker);
|
||||||
const stickers = yield select(selectMapStickers);
|
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case MODES.STICKERS:
|
case MODES.STICKERS:
|
||||||
yield put(
|
yield put(mapAddSticker({ latlng, set, sticker, text: '', angle: 0 }));
|
||||||
mapSet({
|
yield put(setMode(MODES.NONE));
|
||||||
stickers: [
|
|
||||||
...stickers,
|
|
||||||
{
|
|
||||||
latlng,
|
|
||||||
set,
|
|
||||||
sticker,
|
|
||||||
text: "",
|
|
||||||
angle: 0,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
yield put(setMode(MODES.NONE))
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* mapSaga() {
|
// function* changeProviderSaga({ provider }: ReturnType<typeof changeProvider>) {
|
||||||
yield takeEvery(MAP_ACTIONS.MAP_CLICKED, onMapClick);
|
// const { provider: current_provider } = yield select(selectUser);
|
||||||
|
|
||||||
|
// yield put(mapSetProvider(provider));
|
||||||
|
|
||||||
|
// if (current_provider === provider) return;
|
||||||
|
|
||||||
|
// yield put(setChanged(true));
|
||||||
|
|
||||||
|
// return put(setMode(MODES.NONE));
|
||||||
|
// }
|
||||||
|
|
||||||
|
function* setLogoSaga({ logo }: { type: string; logo: string }) {
|
||||||
|
const { mode } = yield select(selectUser);
|
||||||
|
|
||||||
|
yield put(setChanged(true));
|
||||||
|
|
||||||
|
if (mode === MODES.LOGO) {
|
||||||
|
yield put(setMode(MODES.NONE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* replaceAddressIfItsBusy(destination, original) {
|
||||||
|
if (original) {
|
||||||
|
yield put(setAddressOrigin(original));
|
||||||
|
}
|
||||||
|
|
||||||
|
pushPath(`/${destination}/edit`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* loadMapSaga(path) {
|
||||||
|
const {
|
||||||
|
data: { route, error, random_url },
|
||||||
|
}: Unwrap<typeof getStoredMap> = yield call(getStoredMap, { name: path });
|
||||||
|
|
||||||
|
if (route && !error) {
|
||||||
|
// TODO: set initial data
|
||||||
|
// TODO: fit bounds
|
||||||
|
|
||||||
|
yield put(
|
||||||
|
mapSet({
|
||||||
|
provider: route.provider,
|
||||||
|
route: route.route,
|
||||||
|
stickers: route.stickers,
|
||||||
|
title: route.title,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return { route, random_url };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function* startEmptyEditorSaga() {
|
||||||
|
const {
|
||||||
|
user: { id, random_url },
|
||||||
|
provider = DEFAULT_PROVIDER,
|
||||||
|
} = yield select(selectUser);
|
||||||
|
|
||||||
|
// TODO: set owner { id }
|
||||||
|
pushPath(`/${random_url}/edit`);
|
||||||
|
|
||||||
|
yield put(setChanged(false));
|
||||||
|
yield put(setEditing(true));
|
||||||
|
|
||||||
|
return yield call(setReadySaga);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* mapInitSaga() {
|
||||||
|
pushLoaderState(90);
|
||||||
|
|
||||||
|
const { path, mode, hash } = getUrlData();
|
||||||
|
const {
|
||||||
|
provider,
|
||||||
|
user: { id },
|
||||||
|
} = yield select(selectUser);
|
||||||
|
|
||||||
|
yield put(mapSetProvider(provider));
|
||||||
|
|
||||||
|
if (hash && /^#map/.test(hash)) {
|
||||||
|
const [, newUrl] = hash.match(/^#map[:/?!](.*)$/);
|
||||||
|
|
||||||
|
if (newUrl) {
|
||||||
|
yield pushPath(`/${newUrl}`);
|
||||||
|
yield call(setReadySaga);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
const map = yield call(loadMapSaga, path);
|
||||||
|
|
||||||
|
if (map && map.route) {
|
||||||
|
if (mode && mode === 'edit') {
|
||||||
|
if (map && map.route && map.route.owner && mode === 'edit' && map.route.owner !== id) {
|
||||||
|
yield call(setReadySaga);
|
||||||
|
yield call(replaceAddressIfItsBusy, map.random_url, map.address);
|
||||||
|
} else {
|
||||||
|
yield put(setAddressOrigin(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
yield put(setEditing(true));
|
||||||
|
// TODO: start editing
|
||||||
|
} else {
|
||||||
|
yield put(setEditing(false));
|
||||||
|
// TODO: stop editing
|
||||||
|
}
|
||||||
|
|
||||||
|
yield call(setReadySaga);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield call(startEmptyEditorSaga);
|
||||||
|
yield put(setReady(true));
|
||||||
|
|
||||||
|
pushLoaderState(100);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function* setActiveStickerSaga() {
|
||||||
|
yield put(setMode(MODES.STICKERS));
|
||||||
|
}
|
||||||
|
|
||||||
|
function* setTitleSaga({ title }: ReturnType<typeof mapSetTitle>) {
|
||||||
|
if (title) {
|
||||||
|
document.title = `${title} | Редактор маршрутов`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function* clearSaga({ type }) {
|
||||||
|
switch (type) {
|
||||||
|
case USER_ACTIONS.CLEAR_POLY:
|
||||||
|
// TODO: clear router waypoints
|
||||||
|
yield put(
|
||||||
|
mapSet({
|
||||||
|
route: [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case USER_ACTIONS.CLEAR_STICKERS:
|
||||||
|
yield put(
|
||||||
|
mapSet({
|
||||||
|
stickers: [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case USER_ACTIONS.CLEAR_ALL:
|
||||||
|
yield put(setChanged(false));
|
||||||
|
yield put(
|
||||||
|
mapSet({
|
||||||
|
route: [],
|
||||||
|
stickers: [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield put(setActiveSticker(null)); // TODO: move to maps
|
||||||
|
yield put(setMode(MODES.NONE));
|
||||||
|
}
|
||||||
|
|
||||||
|
function* sendSaveRequestSaga({
|
||||||
|
title,
|
||||||
|
address,
|
||||||
|
force,
|
||||||
|
is_public,
|
||||||
|
description,
|
||||||
|
}: ReturnType<typeof sendSaveRequest>) {
|
||||||
|
const { route, stickers, provider } = yield select(selectMap);
|
||||||
|
|
||||||
|
if (!route.length && !stickers.length) {
|
||||||
|
return yield put(setSaveError(TIPS.SAVE_EMPTY)); // TODO: move setSaveError to editor
|
||||||
|
}
|
||||||
|
|
||||||
|
const { logo, distance } = yield select(selectUser);
|
||||||
|
const { token } = yield select(selectUserUser);
|
||||||
|
|
||||||
|
yield put(setSaveLoading(true)); // TODO: move setSaveLoading to maps
|
||||||
|
|
||||||
|
const {
|
||||||
|
result,
|
||||||
|
timeout,
|
||||||
|
cancel,
|
||||||
|
}: {
|
||||||
|
result: Unwrap<typeof postMap>;
|
||||||
|
timeout: boolean;
|
||||||
|
cancel: TakeEffect;
|
||||||
|
} = yield race({
|
||||||
|
result: postMap({
|
||||||
|
token,
|
||||||
|
route,
|
||||||
|
stickers,
|
||||||
|
title,
|
||||||
|
force,
|
||||||
|
address,
|
||||||
|
logo,
|
||||||
|
distance,
|
||||||
|
provider,
|
||||||
|
is_public,
|
||||||
|
description,
|
||||||
|
}),
|
||||||
|
timeout: delay(10000),
|
||||||
|
cancel: take(USER_ACTIONS.RESET_SAVE_DIALOG),
|
||||||
|
});
|
||||||
|
|
||||||
|
yield put(setSaveLoading(false));
|
||||||
|
|
||||||
|
if (cancel) return yield put(setMode(MODES.NONE));
|
||||||
|
|
||||||
|
if (result && result.data.code === 'already_exist') return yield put(setSaveOverwrite()); // TODO: move setSaveOverwrite to editor
|
||||||
|
if (result && result.data.code === 'conflict') return yield put(setSaveError(TIPS.SAVE_EXISTS));
|
||||||
|
if (timeout || !result || !result.data.route || !result.data.route.address)
|
||||||
|
return yield put(setSaveError(TIPS.SAVE_TIMED_OUT));
|
||||||
|
|
||||||
|
return yield put( // TODO: move setSaveSuccess to editor
|
||||||
|
setSaveSuccess({
|
||||||
|
address: result.data.route.address,
|
||||||
|
title: result.data.route.title,
|
||||||
|
is_public: result.data.route.is_public,
|
||||||
|
description: result.data.route.description,
|
||||||
|
|
||||||
|
save_error: TIPS.SAVE_SUCCESS,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function* setSaveSuccessSaga({
|
||||||
|
address,
|
||||||
|
title,
|
||||||
|
is_public,
|
||||||
|
description,
|
||||||
|
}: ReturnType<typeof setSaveSuccess>) {
|
||||||
|
const { id } = yield select(selectUser);
|
||||||
|
const { dialog_active } = yield select(selectUser);
|
||||||
|
|
||||||
|
replacePath(`/${address}/edit`);
|
||||||
|
|
||||||
|
yield put(mapSetTitle(title));
|
||||||
|
yield put(mapSetAddress(address));
|
||||||
|
yield put(mapSetPublic(is_public));
|
||||||
|
yield put(mapSetDescription(description));
|
||||||
|
yield put(setChanged(false));
|
||||||
|
yield put(mapSetOwner({ id }));
|
||||||
|
|
||||||
|
if (dialog_active) {
|
||||||
|
yield call(searchSetSagaWorker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// yield editor.setInitialData();
|
||||||
|
// TODO: set initial data here
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* mapSaga() {
|
||||||
|
yield takeEvery(USER_ACTIONS.SET_ACTIVE_STICKER, setActiveStickerSaga); // TODO: move active sticker to maps
|
||||||
|
yield takeEvery(MAP_ACTIONS.MAP_CLICKED, onMapClick);
|
||||||
|
yield takeEvery(MAP_ACTIONS.SET_TITLE, setTitleSaga);
|
||||||
|
yield takeEvery(USER_ACTIONS.SET_LOGO, setLogoSaga);
|
||||||
|
yield takeLatest(USER_ACTIONS.SEND_SAVE_REQUEST, sendSaveRequestSaga);
|
||||||
|
yield takeLatest(USER_ACTIONS.SET_SAVE_SUCCESS, setSaveSuccessSaga);
|
||||||
|
|
||||||
|
yield takeEvery(
|
||||||
|
// TODO: move all actions to MAP
|
||||||
|
[
|
||||||
|
USER_ACTIONS.CLEAR_POLY,
|
||||||
|
USER_ACTIONS.CLEAR_STICKERS,
|
||||||
|
USER_ACTIONS.CLEAR_ALL,
|
||||||
|
USER_ACTIONS.CLEAR_CANCEL,
|
||||||
|
],
|
||||||
|
clearSaga
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { IState } from "$redux/store";
|
import { IState } from "$redux/store";
|
||||||
|
|
||||||
|
export const selectMap = (state: IState) => state.map;
|
||||||
export const selectMapProvider = (state: IState) => state.map.provider;
|
export const selectMapProvider = (state: IState) => state.map.provider;
|
||||||
export const selectMapRoute= (state: IState) => state.map.route;
|
export const selectMapRoute= (state: IState) => state.map.route;
|
||||||
export const selectMapStickers = (state: IState) => state.map.stickers;
|
export const selectMapStickers = (state: IState) => state.map.stickers;
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,7 @@
|
||||||
import { IState } from '$redux/store'
|
import { IState } from '$redux/store'
|
||||||
|
|
||||||
|
export const selectUser = (state: IState) => state.user;
|
||||||
|
export const selectUserUser = (state: IState) => state.user.user;
|
||||||
export const selectUserEditing = (state: IState) => state.user.editing;
|
export const selectUserEditing = (state: IState) => state.user.editing;
|
||||||
export const selectUserMode = (state: IState) => state.user.mode;
|
export const selectUserMode = (state: IState) => state.user.mode;
|
||||||
export const selectUserActiveSticker = (state: IState) => state.user.activeSticker;
|
export const selectUserActiveSticker = (state: IState) => state.user.activeSticker;
|
|
@ -285,7 +285,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
#canvas {
|
#canvas {
|
||||||
background: #ff3344;
|
background: #eeeeee;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
|
|
|
@ -1,53 +1,72 @@
|
||||||
import { editor } from '$modules/Editor';
|
// import { editor } from '$modules/Editor';
|
||||||
import { COLORS, CLIENT } from '$config/frontend';
|
import { COLORS, CLIENT } from '$config/frontend';
|
||||||
import * as saveAs from 'file-saver';
|
import * as 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 { ILatLng } from "$modules/Stickers";
|
import { ILatLng } from '$modules/Stickers';
|
||||||
import { IStickerDump } from "$modules/Sticker";
|
import { IStickerDump } from '$modules/Sticker';
|
||||||
import { IRootState } from "$redux/user";
|
import { IRootState } from '$redux/user';
|
||||||
import { angleBetweenPoints, angleBetweenPointsRad, findDistancePx, middleCoordPx } from "$utils/geom";
|
import {
|
||||||
import { Point } from "leaflet";
|
angleBetweenPoints,
|
||||||
|
angleBetweenPointsRad,
|
||||||
|
findDistancePx,
|
||||||
|
middleCoordPx,
|
||||||
|
} from '$utils/geom';
|
||||||
|
import { Point } from 'leaflet';
|
||||||
|
import { MainMap } from '$containers/map/Map';
|
||||||
|
|
||||||
export interface ITilePlacement {
|
export interface ITilePlacement {
|
||||||
minX: number,
|
minX: number;
|
||||||
maxX: number,
|
maxX: number;
|
||||||
minY: number,
|
minY: number;
|
||||||
maxY: number,
|
maxY: number;
|
||||||
shiftX: number,
|
shiftX: number;
|
||||||
shiftY: number,
|
shiftY: number;
|
||||||
size: number,
|
size: number;
|
||||||
width: number,
|
width: number;
|
||||||
height: number,
|
height: number;
|
||||||
zoom: number,
|
zoom: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStickerPlacement extends IStickerDump {
|
export interface IStickerPlacement extends IStickerDump {
|
||||||
x: number,
|
x: number;
|
||||||
y: number,
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const latLngToTile = (latlng: { lat: number, lng: number }): { x: number, y: number, z: number } => {
|
const latLngToTile = (latlng: {
|
||||||
const { map } = editor.map;
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
}): { x: number; y: number; z: number } => {
|
||||||
|
const map = MainMap;
|
||||||
const zoom = map.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(Math.floor((1 - Math.log(Math.tan(latlng.lat * Math.PI / 180) + 1 / Math.cos(latlng.lat * Math.PI / 180)) / Math.PI) / 2 * (1 << zoom)));
|
const ytile = Number(
|
||||||
|
Math.floor(
|
||||||
|
((1 -
|
||||||
|
Math.log(
|
||||||
|
Math.tan((latlng.lat * Math.PI) / 180) + 1 / Math.cos((latlng.lat * Math.PI) / 180)
|
||||||
|
) /
|
||||||
|
Math.PI) /
|
||||||
|
2) *
|
||||||
|
(1 << zoom)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return { x: xtile, y: ytile, z: zoom };
|
return { x: xtile, y: ytile, z: zoom };
|
||||||
};
|
};
|
||||||
|
|
||||||
const tileToLatLng = (point: { x: number, y: number }): ILatLng => {
|
const tileToLatLng = (point: { x: number; y: number }): ILatLng => {
|
||||||
const { map } = editor.map;
|
const map = MainMap;
|
||||||
const z = map.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)));
|
||||||
|
|
||||||
return { lat, lng };
|
return { lat, lng };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTilePlacement = (): ITilePlacement => {
|
export const getTilePlacement = (): ITilePlacement => {
|
||||||
const { map } = editor.map;
|
const map = MainMap;
|
||||||
const width = window.innerWidth;
|
const width = window.innerWidth;
|
||||||
const height = window.innerHeight;
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
@ -75,7 +94,7 @@ export const getTilePlacement = (): ITilePlacement => {
|
||||||
minY,
|
minY,
|
||||||
maxY,
|
maxY,
|
||||||
shiftX: tileTransformTranslate.x - msw2.x,
|
shiftX: tileTransformTranslate.x - msw2.x,
|
||||||
shiftY: ((maxY - minY) * 256) - (window.innerHeight + (tileTransformTranslate.y - msw2.y)),
|
shiftY: (maxY - minY) * 256 - (window.innerHeight + (tileTransformTranslate.y - msw2.y)),
|
||||||
size: 256,
|
size: 256,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
@ -83,26 +102,24 @@ export const getTilePlacement = (): ITilePlacement => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPolyPlacement = (): Point[] => (
|
export const getPolyPlacement = (latlngs: ILatLng[]): Point[] =>
|
||||||
(!editor.poly.poly || !editor.poly.poly.getLatLngs() || editor.poly.poly.getLatLngs().length <= 0)
|
latlngs.length === 0
|
||||||
? []
|
? []
|
||||||
: editor.poly.poly.getLatLngs().map((latlng) => ({ ...editor.map.map.latLngToContainerPoint(latlng) }))
|
: latlngs.map(latlng => (MainMap.latLngToContainerPoint(latlng)));
|
||||||
);
|
|
||||||
|
|
||||||
export const getStickersPlacement = (): IStickerPlacement[] => (
|
export const getStickersPlacement = (stickers: IStickerDump[]): IStickerPlacement[] =>
|
||||||
(!editor.stickers || editor.stickers.dumpData().length <= 0)
|
stickers.length === 0
|
||||||
? []
|
? []
|
||||||
: editor.stickers.dumpData().map(sticker => ({
|
: stickers.map(sticker => ({
|
||||||
...sticker,
|
...sticker,
|
||||||
...editor.map.map.latLngToContainerPoint(sticker.latlng),
|
...MainMap.latLngToContainerPoint(sticker.latlng),
|
||||||
}))
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
const getImageSource = (coords: { x: number, y: number, zoom: number }): string => (
|
const getImageSource = (coords: { x: number; y: number; zoom: number }, provider: string): string =>
|
||||||
replaceProviderUrl(editor.getProvider(), coords)
|
replaceProviderUrl(provider, coords);
|
||||||
);
|
|
||||||
|
|
||||||
export const imageFetcher = (source: string): Promise<HTMLImageElement> => new Promise((resolve, reject) => {
|
export const imageFetcher = (source: string): Promise<HTMLImageElement> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
|
||||||
img.crossOrigin = 'anonymous';
|
img.crossOrigin = 'anonymous';
|
||||||
|
@ -110,43 +127,54 @@ export const imageFetcher = (source: string): Promise<HTMLImageElement> => new P
|
||||||
img.onerror = () => reject(img);
|
img.onerror = () => reject(img);
|
||||||
|
|
||||||
img.src = source;
|
img.src = source;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchImages = (ctx: CanvasRenderingContext2D, geometry: ITilePlacement ): Promise<{ x: number, y: number, image: HTMLImageElement }[]> => {
|
export const fetchImages = (
|
||||||
const {
|
ctx: CanvasRenderingContext2D,
|
||||||
minX, maxX, minY, maxY, zoom
|
geometry: ITilePlacement,
|
||||||
} = geometry;
|
provider,
|
||||||
|
): Promise<{ x: number; y: number; image: HTMLImageElement }[]> => {
|
||||||
|
const { minX, maxX, minY, maxY, zoom } = geometry;
|
||||||
|
|
||||||
const images = [];
|
const images = [];
|
||||||
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 }) });
|
images.push({ x, y, source: getImageSource({ x, y, zoom }, provider) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(images.map(({ x, y, source }) => (
|
return Promise.all(
|
||||||
imageFetcher(source).then(image => ({ x, y, image }))
|
images.map(({ x, y, source }) => imageFetcher(source).then(image => ({ x, y, image })))
|
||||||
)));
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const composeImages = (
|
export const composeImages = ({
|
||||||
{ images, geometry, ctx }:
|
images,
|
||||||
{ images: [], geometry: ITilePlacement, ctx: CanvasRenderingContext2D }
|
geometry,
|
||||||
): void => {
|
ctx,
|
||||||
const {
|
}: {
|
||||||
minX, minY, shiftX, shiftY, size
|
images: [];
|
||||||
} = geometry;
|
geometry: ITilePlacement;
|
||||||
|
ctx: CanvasRenderingContext2D;
|
||||||
|
}): void => {
|
||||||
|
const { minX, minY, shiftX, shiftY, size } = geometry;
|
||||||
|
|
||||||
images.map(({ x, y, image }) => {
|
images.map(({ x, y, image }) => {
|
||||||
const posX = ((x - minX) * size) + shiftX;
|
const posX = (x - minX) * size + shiftX;
|
||||||
const posY = ((y - minY) * size) - shiftY;
|
const posY = (y - minY) * size - shiftY;
|
||||||
|
|
||||||
return ctx.drawImage(image, posX, posY, 256, 256);
|
return ctx.drawImage(image, posX, posY, 256, 256);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const composePoly = ({ points, ctx }: { points: Point[], ctx: CanvasRenderingContext2D }): void => {
|
export const composePoly = ({
|
||||||
if (editor.poly.isEmpty) return;
|
points,
|
||||||
|
ctx,
|
||||||
|
}: {
|
||||||
|
points: Point[];
|
||||||
|
ctx: CanvasRenderingContext2D;
|
||||||
|
}): void => {
|
||||||
|
if (points.length === 0) return;
|
||||||
|
|
||||||
let minX = points[0].x;
|
let minX = points[0].x;
|
||||||
let maxX = points[0].x;
|
let maxX = points[0].x;
|
||||||
|
@ -180,16 +208,22 @@ export const composePoly = ({ points, ctx }: { points: Point[], ctx: CanvasRende
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const composeArrows = async ({ points, ctx }: { points: Point[], ctx: CanvasRenderingContext2D }): Promise<boolean[]> => {
|
export const composeArrows = async ({
|
||||||
|
points,
|
||||||
|
ctx,
|
||||||
|
}: {
|
||||||
|
points: Point[];
|
||||||
|
ctx: CanvasRenderingContext2D;
|
||||||
|
}): Promise<boolean[]> => {
|
||||||
const image = await imageFetcher(require('$sprites/arrow.svg'));
|
const image = await imageFetcher(require('$sprites/arrow.svg'));
|
||||||
|
|
||||||
const distances = points.map((point, i) => (
|
const distances = points.map(
|
||||||
(points[i + 1] && findDistancePx(points[i], points[i + 1])) || 0
|
(point, i) => (points[i + 1] && findDistancePx(points[i], points[i + 1])) || 0
|
||||||
));
|
);
|
||||||
|
|
||||||
// we want to annotate at least 5 arrows
|
// we want to annotate at least 5 arrows
|
||||||
const min_arrows = (distances.length >= 5 ? 5 : distances.length - 1);
|
const min_arrows = distances.length >= 5 ? 5 : distances.length - 1;
|
||||||
const min_distance = distances.sort((a, b) => (b - a))[min_arrows];
|
const min_distance = distances.sort((a, b) => b - a)[min_arrows];
|
||||||
|
|
||||||
return points.map((point, i) => {
|
return points.map((point, i) => {
|
||||||
if (!points[i + 1]) return false;
|
if (!points[i + 1]) return false;
|
||||||
|
@ -203,7 +237,7 @@ export const composeArrows = async ({ points, ctx }: { points: Point[], ctx: Can
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(middle.x, middle.y);
|
ctx.translate(middle.x, middle.y);
|
||||||
ctx.rotate((Math.PI * 0.5) - angle);
|
ctx.rotate(Math.PI * 0.5 - angle);
|
||||||
ctx.translate(-middle.x, -middle.y);
|
ctx.translate(-middle.x, -middle.y);
|
||||||
|
|
||||||
ctx.moveTo(middle.x, middle.y);
|
ctx.moveTo(middle.x, middle.y);
|
||||||
|
@ -220,35 +254,33 @@ const measureText = (
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
text: string,
|
text: string,
|
||||||
lineHeight: number
|
lineHeight: number
|
||||||
): { width: number, height: number } => (
|
): { width: number; height: number } =>
|
||||||
text.split('\n').reduce((obj, line) => (
|
text.split('\n').reduce(
|
||||||
{
|
(obj, line) => ({
|
||||||
width: Math.max(ctx.measureText(line).width, obj.width),
|
width: Math.max(ctx.measureText(line).width, obj.width),
|
||||||
height: obj.height + lineHeight,
|
height: obj.height + lineHeight,
|
||||||
}
|
}),
|
||||||
), { width: 0, height: 0 })
|
{ width: 0, height: 0 }
|
||||||
);
|
);
|
||||||
|
|
||||||
const measureDistText = (
|
const measureDistText = (
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
text: string,
|
text: string,
|
||||||
height: number
|
height: number
|
||||||
): { width: number, height: number } => (
|
): { width: number; height: number } => ({
|
||||||
{
|
|
||||||
width: ctx.measureText(text).width,
|
width: ctx.measureText(text).width,
|
||||||
height,
|
height,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const composeStickerArrow = (
|
const composeStickerArrow = (
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
angle: number,
|
angle: number
|
||||||
) => {
|
) => {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(x, y);
|
ctx.translate(x, y);
|
||||||
ctx.rotate(angle + (Math.PI * 0.75));
|
ctx.rotate(angle + Math.PI * 0.75);
|
||||||
ctx.translate(-x, -y);
|
ctx.translate(-x, -y);
|
||||||
ctx.fillStyle = '#ff3344';
|
ctx.fillStyle = '#ff3344';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
@ -261,18 +293,12 @@ const composeStickerArrow = (
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
const measureRect = (
|
const measureRect = (x: number, y: number, width: number, height: number, reversed: boolean) => ({
|
||||||
x: number,
|
rectX: reversed ? x - width - 36 - 10 : x,
|
||||||
y: number,
|
rectY: y - 7 - height / 2,
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
reversed: boolean,
|
|
||||||
) => ({
|
|
||||||
rectX: reversed ? (x - width - 36 - 10) : x,
|
|
||||||
rectY: (y - 7 - (height / 2)),
|
|
||||||
rectW: width + 36 + 10,
|
rectW: width + 36 + 10,
|
||||||
rectH: height + 20,
|
rectH: height + 20,
|
||||||
textX: reversed ? (x - width - 36) : x + 36
|
textX: reversed ? x - width - 36 : x + 36,
|
||||||
});
|
});
|
||||||
|
|
||||||
const measureDistRect = (
|
const measureDistRect = (
|
||||||
|
@ -280,16 +306,24 @@ const measureDistRect = (
|
||||||
y: number,
|
y: number,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
reversed: boolean,
|
reversed: boolean
|
||||||
) => ({
|
) => ({
|
||||||
rectX: reversed ? (x - width - 12) : x - 8,
|
rectX: reversed ? x - width - 12 : x - 8,
|
||||||
rectY: (y - 2 - (height / 2)),
|
rectY: y - 2 - height / 2,
|
||||||
rectW: reversed ? (width + 22) : (width + 20),
|
rectW: reversed ? width + 22 : width + 20,
|
||||||
rectH: height + 4,
|
rectH: height + 4,
|
||||||
textX: reversed ? (x - width - 8) : x + 8
|
textX: reversed ? x - width - 8 : x + 8,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const roundedRect = (ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, fill: string, rad: number = 5): void => {
|
export const roundedRect = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
fill: string,
|
||||||
|
rad: number = 5
|
||||||
|
): void => {
|
||||||
ctx.fillStyle = fill;
|
ctx.fillStyle = fill;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(x, y + rad);
|
ctx.moveTo(x, y + rad);
|
||||||
|
@ -317,7 +351,6 @@ export const roundedRect = (ctx: CanvasRenderingContext2D, x: number, y: number,
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
|
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const composeStickerText = (
|
const composeStickerText = (
|
||||||
|
@ -325,19 +358,23 @@ const composeStickerText = (
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
angle: number,
|
angle: number,
|
||||||
text: string,
|
text: string
|
||||||
): void => {
|
): void => {
|
||||||
const rad = 56;
|
const rad = 56;
|
||||||
const tX = ((Math.cos(angle + Math.PI) * rad) - 30) + x + 28;
|
const tX = Math.cos(angle + Math.PI) * rad - 30 + x + 28;
|
||||||
const tY = ((Math.sin(angle + Math.PI) * rad) - 30) + y + 29;
|
const tY = Math.sin(angle + Math.PI) * rad - 30 + y + 29;
|
||||||
|
|
||||||
ctx.font = '12px "Helvetica Neue", Arial';
|
ctx.font = '12px "Helvetica Neue", Arial';
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
const { width, height } = measureText(ctx, text, 16);
|
const { width, height } = measureText(ctx, text, 16);
|
||||||
|
|
||||||
const {
|
const { rectX, rectY, rectW, rectH, textX } = measureRect(
|
||||||
rectX, rectY, rectW, rectH, textX
|
tX,
|
||||||
} = measureRect(tX, tY, width, height, (angle > -(Math.PI / 2) && angle < (Math.PI / 2)));
|
tY,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
angle > -(Math.PI / 2) && angle < Math.PI / 2
|
||||||
|
);
|
||||||
// rectangle
|
// rectangle
|
||||||
// ctx.fillStyle = '#222222';
|
// ctx.fillStyle = '#222222';
|
||||||
// ctx.beginPath();
|
// ctx.beginPath();
|
||||||
|
@ -353,23 +390,18 @@ const composeStickerText = (
|
||||||
|
|
||||||
// text
|
// text
|
||||||
ctx.fillStyle = 'white';
|
ctx.fillStyle = 'white';
|
||||||
lines.map((line, i) => (
|
lines.map((line, i) => ctx.fillText(line, textX, rectY + 6 + 16 * (i + 1)));
|
||||||
ctx.fillText(
|
|
||||||
line,
|
|
||||||
textX,
|
|
||||||
rectY + 6 + (16 * (i + 1)),
|
|
||||||
)
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const composeDistMark = (
|
export const composeDistMark = ({
|
||||||
{ ctx, points, distance }:
|
ctx,
|
||||||
{
|
points,
|
||||||
ctx: CanvasRenderingContext2D,
|
distance,
|
||||||
points: Point[],
|
}: {
|
||||||
distance: number,
|
ctx: CanvasRenderingContext2D;
|
||||||
}
|
points: Point[];
|
||||||
): void => {
|
distance: number;
|
||||||
|
}): void => {
|
||||||
if (points.length <= 1) return;
|
if (points.length <= 1) return;
|
||||||
|
|
||||||
ctx.font = 'bold 12px "Helvetica Neue", Arial';
|
ctx.font = 'bold 12px "Helvetica Neue", Arial';
|
||||||
|
@ -378,9 +410,13 @@ export const composeDistMark = (
|
||||||
const dist = parseFloat(distance.toFixed(1));
|
const dist = parseFloat(distance.toFixed(1));
|
||||||
const { x, y } = points[points.length - 1];
|
const { x, y } = points[points.length - 1];
|
||||||
const { width, height } = measureDistText(ctx, String(dist), 16);
|
const { width, height } = measureDistText(ctx, String(dist), 16);
|
||||||
const {
|
const { rectX, rectY, rectW, rectH, textX } = measureDistRect(
|
||||||
rectX, rectY, rectW, rectH, textX
|
x,
|
||||||
} = measureDistRect(x, y, width, height, !(angle > -90 && angle < 90));
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
!(angle > -90 && angle < 90)
|
||||||
|
);
|
||||||
|
|
||||||
roundedRect(ctx, rectX, rectY, rectW, rectH, '#ff3344', 2);
|
roundedRect(ctx, rectX, rectY, rectW, rectH, '#ff3344', 2);
|
||||||
|
|
||||||
|
@ -390,11 +426,7 @@ export const composeDistMark = (
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
ctx.fillText(
|
ctx.fillText(String(dist), textX, rectY + 14);
|
||||||
String(dist),
|
|
||||||
textX,
|
|
||||||
rectY + 14,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const composeStickerImage = async (
|
const composeStickerImage = async (
|
||||||
|
@ -403,22 +435,25 @@ const composeStickerImage = async (
|
||||||
y: number,
|
y: number,
|
||||||
angle: number,
|
angle: number,
|
||||||
set: IRootState['activeSticker']['set'],
|
set: IRootState['activeSticker']['set'],
|
||||||
sticker: IRootState['activeSticker']['sticker'],
|
sticker: IRootState['activeSticker']['sticker']
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const rad = 56;
|
const rad = 56;
|
||||||
const tX = ((Math.cos(angle + Math.PI) * rad) - 30) + (x - 8);
|
const tX = Math.cos(angle + Math.PI) * rad - 30 + (x - 8);
|
||||||
const tY = ((Math.sin(angle + Math.PI) * rad) - 30) + (y - 4);
|
const tY = Math.sin(angle + Math.PI) * rad - 30 + (y - 4);
|
||||||
const offsetX = STICKERS[set].layers[sticker].off * 72;
|
const offsetX = STICKERS[set].layers[sticker].off * 72;
|
||||||
|
|
||||||
return imageFetcher(STICKERS[set].url).then(image => (
|
return imageFetcher(STICKERS[set].url).then(image =>
|
||||||
ctx.drawImage(image, offsetX, 0, 72, 72, tX, tY, 72, 72)
|
ctx.drawImage(image, offsetX, 0, 72, 72, tX, tY, 72, 72)
|
||||||
));
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const composeStickers = async (
|
export const composeStickers = async ({
|
||||||
{ stickers, ctx }:
|
stickers,
|
||||||
{ stickers: IStickerPlacement[], ctx: CanvasRenderingContext2D }
|
ctx,
|
||||||
): Promise<void> => {
|
}: {
|
||||||
|
stickers: IStickerPlacement[];
|
||||||
|
ctx: CanvasRenderingContext2D;
|
||||||
|
}): Promise<void> => {
|
||||||
if (!stickers || stickers.length < 0) return;
|
if (!stickers || stickers.length < 0) return;
|
||||||
|
|
||||||
stickers.map(({ x, y, angle, text }) => {
|
stickers.map(({ x, y, angle, text }) => {
|
||||||
|
@ -427,12 +462,12 @@ export const composeStickers = async (
|
||||||
if (text) composeStickerText(ctx, x, y, angle, text);
|
if (text) composeStickerText(ctx, x, y, angle, text);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(stickers.map(({
|
await Promise.all(
|
||||||
x, y, angle, set, sticker
|
stickers.map(({ x, y, angle, set, sticker }) =>
|
||||||
}) => (
|
|
||||||
composeStickerImage(ctx, x, y, angle, set, sticker)
|
composeStickerImage(ctx, x, y, angle, set, sticker)
|
||||||
)));
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const downloadCanvas = (canvas: HTMLCanvasElement, title: IRootState['title']): void => canvas.toBlob(blob => saveAs(blob, title));
|
export const downloadCanvas = (canvas: HTMLCanvasElement, title: IRootState['title']): void =>
|
||||||
|
canvas.toBlob(blob => saveAs(blob, title));
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue