From 0b0b8b9cc09e3054da4a3f8a47607193211a806c Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Mon, 13 Jan 2020 18:00:06 +0700 Subject: [PATCH] added km marks --- src/containers/map/KmMarks/index.tsx | 37 +++++++ src/containers/map/Map/index.tsx | 2 + src/utils/marks.ts | 156 +++++++++++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 src/containers/map/KmMarks/index.tsx create mode 100644 src/utils/marks.ts diff --git a/src/containers/map/KmMarks/index.tsx b/src/containers/map/KmMarks/index.tsx new file mode 100644 index 0000000..f7c5054 --- /dev/null +++ b/src/containers/map/KmMarks/index.tsx @@ -0,0 +1,37 @@ +import React, { FC, useEffect, useState } from 'react'; +import { KmMarksLayer } from '~/utils/marks'; +import { MainMap } from '~/constants/map'; +import { selectMap } from '~/redux/map/selectors'; +import pick from 'ramda/es/pick'; +import { connect } from 'react-redux'; + +const mapStateToProps = state => ({ + map: pick(['route'], selectMap(state)), +}); + +const mapDispatchToProps = {}; +type Props = ReturnType & typeof mapDispatchToProps & {}; + +const KmMarksUnconnected: FC = ({ + map: { route }, +}) => { + const [layer, setLayer] = useState(null); + + useEffect(() => { + const layer = new KmMarksLayer([]); + layer.addTo(MainMap); + setLayer(layer); + return () => MainMap.removeLayer(layer); + }, [MainMap]); + + useEffect(() => { + if (!layer) return; + + layer.setLatLngs(route); + }, [layer, route]); + return null; +}; + +const KmMarks = connect(mapStateToProps, mapDispatchToProps)(KmMarksUnconnected); + +export { KmMarks }; diff --git a/src/containers/map/Map/index.tsx b/src/containers/map/Map/index.tsx index be56cc9..faa91db 100644 --- a/src/containers/map/Map/index.tsx +++ b/src/containers/map/Map/index.tsx @@ -10,6 +10,7 @@ import { Route } from '~/containers/map/Route'; import { Router } from '~/containers/map/Router'; import { TileLayer } from '~/containers/map/TileLayer'; import { Stickers } from '~/containers/map/Stickers'; +import { KmMarks } from '~/containers/map/KmMarks'; import 'leaflet/dist/leaflet.css'; import { selectEditorEditing } from '~/redux/editor/selectors'; @@ -73,6 +74,7 @@ const MapUnconnected: React.FC = ({ + , document.getElementById('canvas') ); diff --git a/src/utils/marks.ts b/src/utils/marks.ts new file mode 100644 index 0000000..6ab49dc --- /dev/null +++ b/src/utils/marks.ts @@ -0,0 +1,156 @@ +import { divIcon, LatLngLiteral, Layer, LayerGroup, Map, marker, Marker } from "leaflet"; +import { arrowClusterIcon, createArrow } from "~/utils/arrow"; +import { MarkerClusterGroup } from 'leaflet.markercluster/dist/leaflet.markercluster-src.js'; +import { allwaysPositiveAngleDeg, angleBetweenPoints, distKm } from "~/utils/geom"; +import classNames from 'classnames'; + +interface KmMarksOptions { + showMiddleMarkers: boolean, + showEndMarker: boolean, + kmMarksStep: number, +} + +class KmMarksLayer extends LayerGroup { + constructor(latlngs?: LatLngLiteral[], options?: KmMarksOptions){ + super(); + + this.options = { + showMiddleMarkers: true, + showEndMarker: true, + kmMarksStep: 10, + ...(options || {}), + } as KmMarksOptions; + } + + setLatLngs = (latlngs: LatLngLiteral[]): void => { + if (!this.map) return; + this.marksLayer.clearLayers(); + this.endMarker.clearLayers(); + + this.distance = 0; + + if (latlngs.length <= 1) return; + + if (this.options.showMiddleMarkers) this.drawMiddleMarkers(latlngs); + if (this.options.showEndMarker) this.drawEndMarker(latlngs); + }; + + drawMiddleMarkers = (latlngs: LatLngLiteral[]) => { + const kmMarks = {}; + let last_km_mark = 0; + + this.distance = latlngs.reduce((dist, current, index) => { + if (index >= latlngs.length - 1) return dist; + + const next = latlngs[index + 1]; + const diff = distKm(current, next); + const sum = dist + diff; + const rounded = Math.floor(sum / this.options.kmMarksStep) * this.options.kmMarksStep; + const count = Math.floor((rounded - last_km_mark) / this.options.kmMarksStep); + + if (rounded > last_km_mark) { + const angle = angleBetweenPoints( + this.map.latLngToContainerPoint(current), + this.map.latLngToContainerPoint(next), + ); + + for (let i = 1; i <= count; i += 1) { + const step = last_km_mark + (i * this.options.kmMarksStep); + const shift = (step - dist) / diff; + + const coords = { + lat: current.lat - ((current.lat - next.lat) * shift), + lng: current.lng - ((current.lng - next.lng) * shift), + }; + + kmMarks[step] = { ...coords, angle }; + this.marksLayer.addLayer(this.createMiddleMarker(coords, angle, step)); + } + + last_km_mark = rounded; + } + + return sum; + }, 0); + + }; + + createMiddleMarker = (latlng: LatLngLiteral, angle: number, distance: number): Marker => marker(latlng, { + draggable: false, + interactive: false, + icon: divIcon({ + html: ` +
+ ${distance} +
+ `, + className: 'leaflet-km-marker', + iconSize: [11, 11], + iconAnchor: [6, 6] + }) + }); + + createEndMarker = (latlng: LatLngLiteral, angle: number, distance: number): Marker => marker(latlng, { + draggable: false, + interactive: false, + icon: divIcon({ + html: ` +
+ ${parseFloat(distance.toFixed(1))} +
+ `, + className: classNames('leaflet-km-marker end-marker', { right: (angle > -90 && angle < 90) }), + iconSize: [11, 11], + iconAnchor: [6, 6] + }), + zIndexOffset: -100, + }); + + drawEndMarker = (latlngs: LatLngLiteral[]): void => { + this.endMarker.clearLayers(); + + const current = latlngs[latlngs.length - 2]; + const next = latlngs[latlngs.length - 1 + ]; + + const angle = angleBetweenPoints( + this.map.latLngToContainerPoint(current), + this.map.latLngToContainerPoint(next), + ); + + this.endMarker.addLayer(this.createEndMarker(next, angle, this.distance)); + }; + + options: KmMarksOptions; + map: Map; + marksLayer: MarkerClusterGroup = new MarkerClusterGroup({ + spiderfyOnMaxZoom: false, + showCoverageOnHover: false, + zoomToBoundsOnClick: false, + animate: false, + maxClusterRadius: 120, + iconCreateFunction: arrowClusterIcon, + }); + endMarker: LayerGroup = new LayerGroup(); + distance: number = 0; +} + + +KmMarksLayer.addInitHook(function () { + this.once('add', (event) => { + if (event.target instanceof KmMarksLayer) { + this.map = event.target._map; + this.marksLayer.addTo(this.map); + this.endMarker.addTo(this.map); + } + }); + + this.once('remove', (event) => { + if (event.target instanceof KmMarksLayer) { + this.marksLayer.removeFrom(this.map); + this.endMarker.removeFrom(this.map); + } + }); +}); + +export { KmMarksLayer }; \ No newline at end of file