Skip to content

Use Case #3: Drag & Drop маршрута (Эффект «Резинки» / Rubber-banding)

Исходное состояние: Есть построенный маршрут А → Б → В. Линия проходит по скучной трассе, а юзер хочет пустить ее через лес неподалеку. Действие: Пользователь наводит курсор на линию, зажимает кнопку мыши/палец и тянет.

Это самый сложный в программной реализации, но самый тактильный и интуитивный инструмент тонкой настройки.


1. Интент (Намерение)

"Я согласен со стартом и финишем, но мне не нравится конкретно этот кусок, который предложил алгоритм. Я хочу его изогнуть, не сломав остальную часть маршрута".

2. Идеальная логика системы (Концепция Якорей)

Самый важный вопрос — какая часть должна тянуться?

  • Правило Якорей: Система должна определить две ближайшие к месту захвата существующие точки. Если пользователь схватил трек на участке между А и Б, то именно они становятся Якорями (Anchors). Точка В и участок Б → В жестко замораживаются.
  • В месте, где пользователь бросил курсор, создается новая точка (X).
  • Алгоритм пересчитывает роутинг ТОЛЬКО для куска А → X → Б.

3. Секрет идеального UX: Формирующие точки (Shaping points)

Это то, что отличает профессиональный продукт от студенческой поделки. Если юзер 5 раз потянул за трек, у него не должно появиться 5 новых полноправных остановок в списке слева ("Точка 1", "Точка 2"). Иначе навигатор во время поездки сведет его с ума голосом: "Вы достигли промежуточной точки".

  • При Drag & Drop должны создаваться «Мягкие» (Формирующие) точки.
  • Они отображаются на самой линии маленьким белым кружком, но в текстовом списке маршрута они скрыты. Их единственная задача — гнуть линию.

4. Визуальный отклик (Фазы UX)

  • Hover (Наведение): Курсор меняется на "руку" ✋. На линии трека под курсором загорается индикатор (жирная белая точка) — сигнал: "Хватай меня".
  • Drag (Тянем):
    • Исходная линия маршрута (А → Б) не исчезает сразу, а становится полупрозрачной (Ghost line) — мы показываем, от чего юзер отказывается.
    • От якоря А к курсору и от курсора к якорю Б тянутся пунктирные прямые линии (по воздуху). Мы не пытаемся пересчитывать маршрут по дорогам прямо во время движения мыши — это вызывает жуткие лаги. Пунктир — идеальный компромисс.
  • Drop (Бросок / Отпускание):
    • Применяется логика Snapping (Прилипание). Точка X сама магнитится к ближайшей легальной дороге.
    • Пунктир пропадает, на долю секунды появляется лоадер, и отрисовывается новый пересчитанный трек по дорогам. Полупрозрачная старая линия пропадает.

5. Антипаттерны при Drag & Drop

  • Глобальный пересчет: Юзер потянул за кусок в начале маршрута, а роутер радостно "оптимизировал" вообще весь трек до самого финиша, уничтожив работу, которую юзер проделал руками 10 минут назад в конце маршрута. Изоляция отрезка должна быть абсолютной!
  • Бросок в "черную дыру": Если юзер бросил курсор на озеро (где нет дорог). Нельзя выдавать Alert "Маршрут не найден" и обнулять действие! Точка должна примагнититься к ближайшей тропе. А если дорог нет в радиусе 1 км, система просто рисует прямую линию (off-grid) и дает Toast-уведомление "Проложен маршрут напрямую без дорог".

Эпики и Задачи (Epics & Tasks)

Декомпозиция Use Case #3 для разработки.

Epic 1: Логика Якорей и Формирующих Точек (Anchors & Shaping Points)

  • Task 1.1: Концепция Shaping Points. В стейте (plannerStore) добавить свойство type: 'shaping' | 'stop' для объекта Waypoint. Обновить UI списка точек слева так, чтобы он фильтровал и не показывал точки типа 'shaping'.
  • Task 1.2: Изоляция роутинга (Segment Isolation). Архитектура роутинга должна быть сегментированной (массив геометрий). При создании новой Shaping Point пересчет (обращение к API) должен происходить строго для сегмента Якорь 1 -> Shaping Point -> Якорь 2. Запретить алгоритмам пересчитывать весь маршрут целиком.
  • Task 1.3: Определение Якорей. Разработать утилиту, которая по координате клика (в пределах геометрии трека) определяет, какому сегменту (и, соответственно, каким двум Якорям) принадлежит этот участок.

Epic 2: Фазы UX взаимодействия (MapLibre Interaction)

  • Task 2.1: Hover State. На слое маршрута отслеживать событие onMouseMove. Менять курсор и рендерить на треке под курсором динамический маркер "Точка захвата" (Grab Indicator).
  • Task 2.2: Drag State (Пунктиры). При onMouseDown на "Точке захвата":
    • Слой текущего редактируемого сегмента переводится в opacity: 0.3 (Ghost line).
    • Начинает рендериться временный слой (line-dasharray) с двумя прямыми линиями: [Якорь 1, Курсор] и [Курсор, Якорь 2].
    • Отключить стандартный pan карты при перетаскивании.
  • Task 2.3: Snapping и обработка "Черной дыры" (Drop). При onMouseUp:
    • Точка X передается в API как новая транзитная остановка.
    • Если роутер (например, BRouter) не может найти путь и возвращает ошибку (нет дорог), переключить именно этот сегмент (Якорь 1 -> X -> Якорь 2) в режим Direct (По прямой).
    • Вызвать Toast-уведомление "Проложен маршрут напрямую без дорог".

Epic 3: Оптимизация производительности

  • Task 3.1: Запрет реалтайм-запросов (No API Calls on Drag). Строго запретить вызовы к роутинг-бэкенду в фазе Drag (движение мыши). API вызывается только один раз на событии onMouseUp (отпускание курсора).
  • Task 3.2: Анимация загрузки. На время API запроса (после Drop) показывать UI-лоадер в месте броска или в панели точек, чтобы пользователь понимал, что запрос в обработке.