optimized arrows a little bit

This commit is contained in:
Fedor Katurov 2020-01-16 16:19:08 +07:00
parent de2b747a20
commit 58427e7017
3 changed files with 336 additions and 44 deletions

View file

@ -76,7 +76,7 @@ const MapUnconnected: React.FC<IProps> = ({
<Route /> <Route />
<Router /> <Router />
<KmMarks /> <KmMarks />
<Arrows /> <Arrows />
</div>, </div>,

View file

@ -1,24 +1,27 @@
import { divIcon, LatLngLiteral, Marker, marker, DivIcon } from "leaflet"; import { divIcon, LatLngLiteral, Marker, marker, DivIcon } from 'leaflet';
const arrow_image = require('~/sprites/arrow.svg'); const arrow_image = require('~/sprites/arrow.svg');
// <use xlink:href="#path-arrow" transform="scale(2) translate(5 -2)"/> export const createArrowIcon = (angle: number) =>
export const createArrow = (latlng: LatLngLiteral, angle: number): Marker => marker(latlng, { divIcon({
draggable: false,
interactive: false,
icon: divIcon({
html: ` html: `
<div class="leaflet-arrow" style="transform: rotate(${angle}deg);"> <div class="leaflet-arrow" style="transform: rotate(${angle}deg);">
<svg width="48" height="48" preserveAspectRatio="xMidYMid"> <svg width="48" height="48" preserveAspectRatio="xMidYMid">
<image xlink:href="${arrow_image}" x="0" y="0" width="48" height="48"/> <image xlink:href="${arrow_image}" x="0" y="0" width="48" height="48"/>
</svg> </svg>
</div> </div>
`, `,
className: 'leaflet-arrow-icon', className: 'leaflet-arrow-icon',
iconSize: [11, 11], iconSize: [11, 11],
iconAnchor: [6, 6] iconAnchor: [6, 6],
}) });
});
export const createArrow = (latlng: LatLngLiteral, angle: number): Marker =>
new Marker(latlng, {
draggable: false,
interactive: false,
icon: createArrowIcon(angle),
});
export const arrowClusterIcon = (cluster): DivIcon => { export const arrowClusterIcon = (cluster): DivIcon => {
const markers = cluster.getAllChildMarkers(); const markers = cluster.getAllChildMarkers();

View file

@ -1,61 +1,350 @@
import { LatLngLiteral, LayerGroup, Map } from "leaflet"; import { LatLngLiteral, LayerGroup, Map, LatLng, Marker, marker } from 'leaflet';
import { arrowClusterIcon, createArrow } from "~/utils/arrow"; import { arrowClusterIcon, createArrow, createArrowIcon } from '~/utils/arrow';
import { MarkerClusterGroup } from 'leaflet.markercluster/dist/leaflet.markercluster-src.js'; import { MarkerClusterGroup } from 'leaflet.markercluster/dist/leaflet.markercluster-src.js';
import { angleBetweenPoints, dist2, middleCoord } from "~/utils/geom"; import { angleBetweenPoints, dist2, middleCoord } from '~/utils/geom';
interface MidPoint {
latlng: LatLngLiteral;
angle: number;
}
// interface IPrevState {
// route: LatLngLiteral[];
// markers: Marker[];
// midpoints: MidPoint[];
// }
class ArrowsLayer extends LayerGroup { class ArrowsLayer extends LayerGroup {
constructor(props){ /*
without remove optimization
first:
recalc: 5.469970703125ms
remove: 0.203857421875ms
add: 53.658935546875ms
total: 60.986083984375ms
last:
recalc: 0.010986328125ms
remove: 0.220947265625ms
add: 0.580078125ms
total: 2.721923828125ms
with remove optimization
*/
constructor(props) {
super(props); super(props);
} }
setLatLngs = (latlngs: LatLngLiteral[]): void => { getChangeIndex = (prev: LatLngLiteral[], next: LatLngLiteral[]): number => {
const changed_at = next.findIndex(
(item, index) => !prev[index] || prev[index].lat != item.lat || prev[index].lng != item.lng
);
return changed_at >= 0 ? changed_at : next.length;
};
// Reacreating all the markers
setLatLngs = (route: LatLngLiteral[]): void => {
if (!this.map) return; if (!this.map) return;
this.arrowLayer.clearLayers(); this.arrowLayer.clearLayers();
if (latlngs.length === 0) return; if (route.length === 0) return;
const midpoints = latlngs.reduce((res, latlng, i) => ( const midpoints = route.reduce(
latlngs[i + 1] && dist2(latlngs[i], latlngs[i + 1]) > 0.00005 (res, latlng, i) =>
? [ route[i + 1] && dist2(route[i], route[i + 1]) > 0.0001
...res, ? [
{ ...res,
latlng: middleCoord(latlngs[i], latlngs[i + 1]), createArrow(
angle: angleBetweenPoints( middleCoord(route[i], route[i + 1]),
this.map.latLngToContainerPoint(latlngs[i]), angleBetweenPoints(
this.map.latLngToContainerPoint(latlngs[i + 1]) this.map.latLngToContainerPoint(route[i]),
), this.map.latLngToContainerPoint(route[i + 1])
} )
] ),
: res ]
), []); : res,
[]
);
midpoints.forEach(({ latlng, angle }) => ( this.arrowLayer.addLayers(midpoints);
this.arrowLayer.addLayer(createArrow(latlng, angle))
));
}; };
// Tries to detect if marker changed for every marker starting from changed_at
// setLatLngs = (route: LatLngLiteral[]): void => {
// if (!this.map) return;
// // this.arrowLayer.clearLayers();
// if (route.length === 0) return;
// console.time('total');
// const changed_at = this.getChangeIndex(this.prevState.route, route);
// console.log('changed at:', changed_at);
// console.log('recalc:', route.length - changed_at);
// const midpoints = this.prevState.midpoints.slice(0, changed_at - 1);
// const markers = this.prevState.markers.slice(0, changed_at - 1);
// console.time('recalc');
// for (let i = changed_at; i < route.length; i += 1) {
// const point =
// route[i + 1] && dist2(route[i], route[i + 1]) > 0.00005
// ? {
// latlng: middleCoord(route[i], route[i + 1]),
// angle: angleBetweenPoints(
// this.map.latLngToContainerPoint(route[i]),
// this.map.latLngToContainerPoint(route[i + 1])
// ),
// }
// : null;
// if (this.prevState.markers[i] && !point) {
// // the marker is gone
// this.arrowLayer.removeLayer(this.prevState.markers[i]);
// markers.push(null);
// }
// if (this.prevState.markers[i] && point) {
// // marker changed / created
// const is_same =
// this.prevState.markers[i] &&
// this.prevState.midpoints[i] &&
// this.prevState.midpoints[i].latlng.lat === point.latlng.lat &&
// this.prevState.midpoints[i].latlng.lng === point.latlng.lng;
// if (!is_same) {
// console.log('not same');
// this.prevState.markers[i].setLatLng(point.latlng);
// this.prevState.markers[i].setIcon(createArrowIcon(point.angle));
// }
// markers.push(this.prevState.markers[i]);
// }
// if (!this.prevState.markers[i] && point) {
// // new marker
// const marker = createArrow(point.latlng, point.angle);
// this.arrowLayer.addLayer(marker);
// markers.push(marker);
// }
// midpoints.push(point);
// }
// console.timeEnd('recalc');
// this.prevState = {
// route,
// markers,
// midpoints,
// };
// console.timeEnd('total');
// };
// Only creates from changed item to the end. Buggy when trying to delete and add points in the middle
// setLatLngs = (route: LatLngLiteral[]): void => {
// if (!this.map) return;
// // this.arrowLayer.clearLayers();
// if (route.length === 0) return;
// console.time('total');
// const changed_at = this.getChangeIndex(this.prevState.route, route);
// console.log('changed at:', changed_at);
// console.log('recalc:', route.length - changed_at);
// const midpoints = this.prevState.midpoints.slice(0, changed_at - 1);
// const markers = this.prevState.markers.slice(0, changed_at - 1);
// console.time('recalc');
// for (let i = changed_at; i < route.length; i += 1) {
// const point =
// route[i + 1] && dist2(route[i], route[i + 1]) > 0.00005
// ? {
// latlng: middleCoord(route[i], route[i + 1]),
// angle: angleBetweenPoints(
// this.map.latLngToContainerPoint(route[i]),
// this.map.latLngToContainerPoint(route[i + 1])
// ),
// }
// : null;
// const marker = point ? createArrow(point.latlng, point.angle) : null;
// midpoints.push(point);
// markers.push(marker);
// }
// console.timeEnd('recalc');
// console.time('remove');
// this.arrowLayer.removeLayers(
// this.prevState.markers
// .slice(changed_at - 1, this.prevState.markers.length - 1)
// .filter(el => !!el)
// );
// console.timeEnd('remove');
// this.prevState = {
// route,
// markers,
// midpoints,
// };
// console.time('add');
// this.arrowLayer.addLayers(markers.filter(el => !!el));
// console.timeEnd('add');
// console.timeEnd('total');
// };
// TODO: iterate through all the route and detect if marker created, changed or deleted. Skip getting changed_at
// TODO: try to figure why its not updated properly when you add / delete points in the middle
// setLatLngs = (route: LatLngLiteral[]): void => {
// if (!this.map) return;
// if (route.length === 0) return;
// const newState: IPrevState = {
// route,
// markers: [null],
// };
// for (let i = 1; i < route.length; i += 1) {
// const current = route[i];
// const previous = route[i - 1];
// const is_new = !this.prevState.route[i];
// const is_changed =
// this.prevState.route[i] &&
// (this.prevState.route[i].lat !== current.lat ||
// this.prevState.route[i].lng !== current.lng ||
// this.prevState.route[i - 1].lng !== previous.lng ||
// this.prevState.route[i - 1].lng !== previous.lng);
// const need_to_add = dist2(route[i], route[i - 1]) > 0.00005;
// if (is_new) {
// const marker = need_to_add
// ? createArrow(
// middleCoord(route[i], route[i - 1]),
// angleBetweenPoints(
// this.map.latLngToContainerPoint(route[i]),
// this.map.latLngToContainerPoint(route[i - 1])
// )
// )
// : null;
// console.log(i, marker ? 'new create' : 'new skip');
// if (marker) {
// this.arrowLayer.addLayer(marker);
// }
// newState.markers.push(marker);
// continue;
// }
// if (is_changed) {
// const middle = middleCoord(route[i], route[i - 1]);
// const angle = angleBetweenPoints(
// this.map.latLngToContainerPoint(route[i]),
// this.map.latLngToContainerPoint(route[i - 1])
// );
// if (need_to_add && this.prevState.markers[i]) {
// console.log(i, 'change');
// this.prevState.markers[i].setLatLng(middle);
// this.prevState.markers[i].setIcon(createArrowIcon(angle));
// newState.markers.push(this.prevState.markers[i]);
// continue;
// }
// if (need_to_add && !this.prevState.markers[i]) {
// console.log(i, 'change create');
// const marker = createArrow(middle, angle);
// this.arrowLayer.addLayer(marker);
// newState.markers.push(marker);
// continue;
// }
// if (!need_to_add && this.prevState.markers[i]) {
// console.log(i, 'change remove');
// this.arrowLayer.removeLayer(this.prevState.markers[i]);
// newState.markers.push(null);
// continue;
// }
// if (!need_to_add && !this.prevState.markers[i]) {
// console.log(i, 'change skip');
// newState.markers.push(null);
// continue;
// }
// }
// if (!is_new && !is_changed) {
// console.log(i, 'not changed');
// newState.markers.push(this.prevState.markers[i]);
// }
// }
// console.log('------', newState.markers);
// if (newState.markers.length < this.prevState.markers.length) {
// this.arrowLayer.removeLayers(
// this.prevState.markers
// .slice(
// this.prevState.markers.length - newState.markers.length,
// this.prevState.markers.length - 1
// )
// .filter(el => !!el)
// );
// }
// this.prevState = newState;
// };
map: Map; map: Map;
arrowLayer: MarkerClusterGroup = new MarkerClusterGroup({ arrowLayer: MarkerClusterGroup = new MarkerClusterGroup({
spiderfyOnMaxZoom: false, spiderfyOnMaxZoom: false,
showCoverageOnHover: false, showCoverageOnHover: false,
zoomToBoundsOnClick: false, zoomToBoundsOnClick: false,
animate: false, animate: false,
maxClusterRadius: 120, maxClusterRadius: 120, // 120
iconCreateFunction: arrowClusterIcon, iconCreateFunction: arrowClusterIcon,
}); });
// prevState: IPrevState = {
// route: [],
// markers: [],
// midpoints: [],
// // distances: [],
// };
layers: Marker<any>[] = [];
} }
ArrowsLayer.addInitHook(function() {
ArrowsLayer.addInitHook(function () { this.once('add', event => {
this.once('add', (event) => {
if (event.target instanceof ArrowsLayer) { if (event.target instanceof ArrowsLayer) {
this.map = event.target._map; this.map = event.target._map;
this.arrowLayer.addTo(this.map); this.arrowLayer.addTo(this.map);
} }
}); });
this.once('remove', (event) => { this.once('remove', event => {
if (event.target instanceof ArrowsLayer) { if (event.target instanceof ArrowsLayer) {
this.arrowLayer.removeFrom(this.map); this.arrowLayer.removeFrom(this.map);
} }