Route engine and widget controller architecture

This document explains the split between:

  • RouteEngine (src/here_search_demo/route_engine.py)

  • RouteController (src/here_search_demo/widgets/route.py)

The goal is to keep route logic reusable and head-agnostic while preserving map-specific UX in the widget layer.

Layering

OneBoxCore / OneBoxMap
        │
        ├─ RouteEngine        (head-agnostic route model + routing service)
        └─ RouteController    (ipyleaflet adapter + rendering + controls)

RouteEngine (head-agnostic)

RouteEngine owns route state and routing computations without importing ipyleaflet or ipywidgets.

Responsibilities

  • Route state:

    • start_position, stop_position, current_position, future_position

    • width, mins_from_pos

    • ranking_mode (all_along, travel_time)

  • Derived context:

    • search_at_position

    • route_flexpolyline, search_flexpolyline

    • route_summary_length, waypoints_count

  • Routing data retrieval and cache management:

    • update_route_attributes()

    • _route_cache reuse

  • Route option mutators:

    • set_route_start, set_route_stop, set_current_position

    • set_route_width

    • Ranking options via the all_along / minimal_detour properties

Why it exists

  • Reuse route-aware behavior outside map notebooks.

  • Make route logic testable in pure unit tests.

  • Keep OneBoxCore extensible for alternative heads.

RouteController (map adapter)

RouteController remains the map-facing layer used by PositionMap / OneBoxMap.

Responsibilities

  • Rendering route geometry and markers on ipyleaflet.

  • Managing UI controls and map callbacks.

  • Scheduling map updates (fit_bounds, detour overlays, marker refresh).

  • Exposing route state by delegating to the owned RouteEngine (the single source of truth) through read/write properties.

Boundary rule

  • If logic can run without widget objects, it belongs in RouteEngine.

  • If logic manipulates map layers, controls, or map event UX, it belongs in RouteController.

Data flow

  1. User sets route points/options from the map controls.

  2. RouteController updates RouteEngine.

  3. RouteEngine refreshes route attributes and cache.

  4. RouteController renders/updates map layers, reading engine state through its delegating properties.

  5. OneBoxMap._get_context() reads route-derived fields (polyline, width, all_along, search_at_position) for search requests.

Detour reranking interaction

DetourRanker remains a separate head-agnostic component. It can reuse route cache data produced by route acquisition so detour calculations avoid redundant routing calls.

Testing strategy

  • tests/test_route_engine.py: headless route model/service behavior.

  • tests/test_route.py: map adapter rendering and integration behavior.

  • tests/test_widgets_app.py: route-derived request context and rerank trigger behavior.