perliedman / leaflet-routing-machine

Control for routing in Leaflet
https://www.liedman.net/leaflet-routing-machine/
Other
1.06k stars 347 forks source link

Unexpected behavior when zooming #682

Closed LLStudent83 closed 1 year ago

LLStudent83 commented 1 year ago

Good afternoon. I ask for help. There is a routing Machine (RM) component:

/* eslint-disable react-hooks/rules-of-hooks */
import { useEffect, useRef } from 'react';
import L, { Icon } from 'leaflet';
import { useLeafletContext } from '@react-leaflet/core';
import 'leaflet-routing-machine';
import { useAppDispatch } from '../../../../hooks/redux';
import { setDistanceInRouteCreator } from '../../../../redux/Map/MapSearchReducer/actions/map';

const createMarker = (i, start, n, dataIconsForMap) => {
  if (!dataIconsForMap) {
    return null;
  }
  let marker_icon = null;
  if (i === 0) {
    // This is the first marker, indicating start
    marker_icon = dataIconsForMap[1].icon;
  } else if (i === 1) {
    // This is the last marker indicating destination
    marker_icon = dataIconsForMap[0].icon;
  }
  const marker = L.marker(start.latLng, {
    draggable: true,
    bounceOnAdd: false,
    bounceOnAddOptions: {
      duration: 1000,
      height: 800,
    },
    icon: marker_icon,
  });
  return marker;
};

const createRoutineMachineLayer = (props, context) => {
  const { constructionCoord, projectCoord, dataIconsForMap, color, routeId, setBuildingRoute, vehicle } = props;
  const dispatch = useAppDispatch();

  const plan = L.Routing.plan([L.latLng(constructionCoord), L.latLng(projectCoord)], {
    // reverseWaypoints: true,
    createMarker: (i, start, n, dataIconsForMap) => createMarker(i, start, n, dataIconsForMap),
  });

  const routingControl = L.Routing.control({
    // waypoints: [L.latLng(constructionCoord), L.latLng(projectCoord)],
    plan,

    router: new L.Routing.OSRMv1({
      serviceUrl:
        vehicle === 'foot'
          ? 'https://routing.openstreetmap.de/routed-foot/route/v1'
          : 'https://router.project-osrm.org/route/v1',
    }),

    lineOptions: {
      styles: [
        { color: `${color}`, opacity: 1, weight: 8 },
        { color: 'white', opacity: 0.3, weight: 6 },
      ],
      addWaypoints: true, // разрешает добавлять путевые точки к маршруту
    },
    routeWhileDragging: false, // маршрут будет меняться онлайн при перетаскивании точек
    language: 'ru',
    addWaypoints: false, // false
    fitSelectedRoutes: false, // убирает автоцентрирование карты на маршруте
    routeDragInterval: 500,
    useZoomParameter: false, // пересчитывается или нет маршрут при увеличении карты
    showAlternatives: false, // показывает альтернативный маршрут или нет

    show: false, // показывает текстовую расшифровку маршрута;
    draggableWaypoints: true,
  });

  routingControl.on('routingstart', () => {
    context.map.getZoom() < 15 && setBuildingRoute(true); // устраняет БАГ с вечным Loader
    console.log('установили лоадер routingstart', context.map.getZoom());
  });

  routingControl.on('routesfound', (e) => {
    setBuildingRoute(false);
    console.log('удалили лоадер routesfound');

    const { routes } = e;
    const { summary } = routes[0];
    const distance = summary.totalDistance.toFixed();
    dispatch(setDistanceInRouteCreator({ distance, routeId }));
  });

  routingControl.on('routingerror', (e) => {
    console.log('удалили лоадер routingerror');

    setBuildingRoute(false);
    return new Error('При построении маршрута произошла ошибка', e);
  });

  return routingControl;
};

function updateRoutineMachineLayer(instance, props, prevProps) {
  // instance это createElement(props, context)
  instance.on('routesfound', (e) => {
    props.setBuildingRoute(false);
    console.log('удалили лоадер routesfound из update');
  });

  if (props.constructionCoord !== prevProps.constructionCoord) {
    instance.setWaypoints([L.latLng(props.constructionCoord), L.latLng(props.projectCoord)]);
  }
  if (props.vehicle !== prevProps.vehicle) {
    const router = instance.getRouter();

    router.options.serviceUrl =
      props.vehicle === 'foot'
        ? 'https://routing.openstreetmap.de/routed-foot/route/v1'
        : 'https://router.project-osrm.org/route/v1';
    instance.route();
  }
}

// при монтировании компонента добавляет елемент control на карту
// при размонтировании удаляет его с карты
function useLayerLifecycle(element, context) {
  useEffect(() => {
    const container = context.layerContainer ?? context.map;
    container.addControl(element);
    return () => {
      context.layerContainer ? context.layerContainer.removeControl(element) : context.map.removeControl(element);
    };
  }, [context, element]);
}

// --------------------------- createElementHook ---------------------
// возвращает функцию которая при изменении instance, props, context начинает сравнивать props до и после.
// если props изменился то вызывает updateElement который в свою очередь возвращает новый маршрут
export function createElementHook(createElement, updateElement) {
  if (updateElement == null) {
    return function useImmutableLeafletElement(props, context) {
      return useRef(createElement(props, context));
    };
  }
  return function useMutableLeafletElement(props, context) {
    const elementRef = useRef(createElement(props, context));
    const propsRef = useRef(props);
    const instance = elementRef.current;

    useEffect(() => {
      if (propsRef.current !== props) {
        updateElement(instance, props, propsRef.current);
        propsRef.current = props;
      }
    }, [instance, props, context]);

    return elementRef;
  };
}

// функция создает новый элемент consrol с возможностью обрабатывать изменения props-ов
// и рпвязывает его к props и context
function createControlComponent(createInstance, updateInstance, props) {
  const context = useLeafletContext();
  const useElement = createElementHook(createInstance, updateInstance);
  const elementRef = useElement(props, context);
  useLayerLifecycle(elementRef.current, context);
}

function RoutingMachine(props) {
  createControlComponent(createRoutineMachineLayer, updateRoutineMachineLayer, props);
  return null;
}

export default RoutingMachine;

I listen to the "routingstart" and "routesfound" events to install and remove the loader. The problem is that after building the route with map = 8, RM sends a request to the server:

URL запроса: https://router.project-osrm.org/route/v1/driving/60.51956176757813,56.79049061895053;60.68435668945313,56.99077873935127?overview=full&alternatives=true&steps=true&hints=P66Ngb825YMAAAAAWgAAABMAAAAAAAAAAAAAAGpsFkL_5_9AAAAAAAAAAABaAAAAEwAAAAAAAAC48gAACnWbAwGOYgOKdJsD241iAwIAfwjMqZWo;Pnw9iMwYl5AAAQAAZAEAAEcBAAChAgAA9NLWQoaAFEPIdwhD_4mMQwABAABkAQAARwEAAKECAAC48gAAivmdAyGaZQNF-J0DO5xlAwIAfwTMqZWo
Метод запроса: GET
Код статуса: 200 
Payload:
overview: full
alternatives: true
steps: true
hints: P66Ngb825YMAAAAAWgAAABMAAAAAAAAAAAAAAGpsFkL_5_9AAAAAAAAAAABaAAAAEwAAAAAAAAC48gAACnWbAwGOYgOKdJsD241iAwIAfwjMqZWo;Pnw9iMwYl5AAAQAAZAEAAEcBAAChAgAA9NLWQoaAFEPIdwhD_4mMQwABAABkAQAARwEAAKECAAC48gAAivmdAyGaZQNF-J0DO5xlAwIAfwTMqZWo

and receives the following response:

{code: "Ok", routes: [{,…}], waypoints: [{,…}, {,…}]}
code: "Ok"
routes: [{,…}]
0: {,…}
distance: 29758
duration: 2010.8
geometry: ".......lots of letters........"
legs: [,…]
0: {steps: [{geometry: "y{ryIagkpJEBG@AP_BnDCFEH",…}, {geometry: "s_syIg`kpJSc@{C_Ha@}@",…}, {,…},…],…}
weight: 2010.8
weight_name: "routability"
waypoints: [{,…}, {,…}]

in this case, the "routesfound" event does not roll out and the application freezes with the loader. Please tell me what could be the problem?

curtisy1 commented 1 year ago

LRM by default only fires a routesfound event, when requesting the whole instruction set of the route. When zooming, the router sets a geometryOnly flag, which in turn leads to the routesfound event being ignored.

If you wish to change this behaviour, the simplest way would be changing the _onZoomEnd function

LLStudent83 commented 1 year ago

Good afternoon Alex. I still have a bad understanding of what is happening under the hood of the routing Machine. Based on the example from tutorial https://www.liedman.net/leaflet-routing-machine/tutorials/interaction / I wrote this:

const NewControl = L.Routing.Control.extend({
  _onZoomEnd() {
    if (!this._selectedRoute || !this._router.requiresMoreDetail) {
      return;
    }

    const map = this._map;
    if (this._router.requiresMoreDetail(this._selectedRoute, map.getZoom(), map.getBounds())) {
      this.route({
        callback: L.bind((err, routes) => {
          let i;
          if (!err) {
            for (i = 0; i < routes.length; i++) {
              this._routes[i].properties = routes[i].properties;
            }
            this._updateLineCallback(err, routes);
          }
        }, this),
        simplifyGeometry: false,
        geometryOnly: false,
      });
    }
  },
});

onst createRoutineMachineLayer = (props, context) => {
  const { constructionCoord, projectCoord, dataIconsForMap, color, routeId, setBuildingRoute, vehicle } = props;
  const dispatch = useAppDispatch();

  const plan = L.Routing.plan([L.latLng(constructionCoord), L.latLng(projectCoord)], {
    createMarker: (i, start, n, dataIconsForMap) => createMarker(i, start, n, dataIconsForMap),
  });

  // const routingControl = L.Routing.control({
  const routingControl = new NewControl({
    plan,

    router: new L.Routing.OSRMv1({
      serviceUrl:
        vehicle === 'foot'
          ? 'https://routing.openstreetmap.de/routed-foot/route/v1'
          : 'https://router.project-osrm.org/route/v1',
    }),
..........................................
}

Please tell me where I made a mistake?

curtisy1 commented 1 year ago

This looks correct to me. Does it not work after zooming out? Would it be possible for you to create a codepen or codesandbox example so I could help in debugging? A very basic, stripped down version of your code that shows the problem is fine

LLStudent83 commented 1 year ago

Well I will try to do it soon.

LLStudent83 commented 1 year ago

Hello.After processing the "routingstart" event when the map zooms up, the code execution reaches line 310 where the callback is called. The code does not reach line 327. Therefore, it does not matter what value geometry Only has. And it may also be important, routing start is not called every time the map is zoomed. Maybe this will give you some thoughts? There is no time to create a code for research yet. Thank you so much for your time.

LLStudent83 commented 1 year ago
const NewControl = L.Routing.Control.extend({
  _onZoomEnd() {
    if (!this._selectedRoute || !this._router.requiresMoreDetail) {
      return;
    }

    const map = this._map;
    if (this._router.requiresMoreDetail(this._selectedRoute, map.getZoom(), map.getBounds())) {
      this.route({
        callback: L.bind((err, routes) => {
          let i;
          if (!err) {
            for (i = 0; i < routes.length; i++) {
              this._routes[i].properties = routes[i].properties;
            }
            this._updateLineCallback(err, routes);
          }
          this.fire('routesfound', { waypoints: routes[0].waypoints, routes });
        }, this),
        simplifyGeometry: false,
        geometryOnly: false,
      });
    }
  },
});

I wrote it like this. The problem is gone. But I'm not sure that this won't lead to some problems in the future.

curtisy1 commented 1 year ago

Oh yeah, you are absolutely right! Sorry about that. The routesfound event is not used anywhere in LRM itself, so the code should work

LLStudent83 commented 1 year ago

Alex, forgive my curiosity. But I want to know why the "routingstart" event is triggered when zooming?

curtisy1 commented 1 year ago

That's because zooming changes the required detail of the route, so it needs to be recalculated. The code responsible can be found here. Since this method is called on zoom and triggers another route request, as in your code above, it will also trigger a routingstart event once the route request starts

LLStudent83 commented 1 year ago

Good morning Alex. Now it's clear. Thank you for your time. I hope our questions and answers will be useful to someone else