.. _junctions:

==========================================
Junctions and the transition matrix
==========================================

A **junction** is any point where two or more legs share an endpoint.
Today's projects often have several -- a Y where two routes converge,
an X where two through-routes cross, a chain of straight legs sharing
intermediate waypoints.  How traffic *splits* at these junctions matters
for collision risk: a 70%/30% split between two onward legs implies a
very different collision pattern than a 50/50 one, and not every leg
that *touches* a junction necessarily *exchanges* traffic with every
other.

OMRAT models this with a per-junction **transition matrix**

.. math::

   T[\text{in\_leg}][\text{out\_leg}] = \text{share of traffic continuing onto out\_leg}

where each row sums to 1.0.  Rows are populated from one of three
sources, in order of preference:

#. **AIS-derived** -- when a database connection is configured, OMRAT
   walks AIS tracks across each leg pair near the junction and
   normalises the per-MMSI counts.  This is the most accurate source.
#. **Geometry-derived** -- when AIS is unavailable, a fallback
   heuristic scores out-legs by their deflection angle from the
   inbound bearing (``exp(-deflection / 30 deg)``) and normalises.
   Straight continuations get the highest share, hairpins the lowest.
#. **User-edited** -- entered or tweaked via
   **Settings > Junction transition matrix...**, marked
   ``source="user"``, and preserved across subsequent
   *Update all distributions* runs.


How a transition matrix changes the calculation
================================================

Two ship-collision sub-models become matrix-aware:

Bend collisions
---------------

Today, bend collision frequency on a leg is computed from a single
``bend_angle`` field plus the leg's total traffic.  When a junction
exists at the leg's end and has a populated matrix, the model instead
iterates over each ``(this_leg -> other_leg)`` matrix row, computing
the deflection angle between inbound and outbound bearings and
weighting traffic by the matrix share.  Pure-continuation pairs
(no deflection) drop out, and split-flow junctions naturally produce
multiple bend contributions.

If a leg has no junction at its end, or the junction has no matrix,
the legacy single-bend path is used (no behaviour change).

Crossing / merging collisions
------------------------------

For each leg pair sharing a junction, the contribution to crossing /
merging frequency is multiplied by a junction-aware **conflict factor**:

.. math::

   f = 1 - T[L_1][L_2] \cdot T[L_2][L_1]

Reasoning: when 100% of leg 1 continues onto leg 2 *and* 100% of leg 2
continues onto leg 1, the only encounter at the junction is a
head-on/overtaking exchange that the per-leg models already capture --
the crossing/merging adjustment is 0.  Any divergence (some traffic
going elsewhere) re-introduces a junction-level conflict, hence the
multiplicative complement.  Defaults to 1.0 when no matrix is set.


Validating routes before computing
====================================

Pressing **Update all distributions** runs a route-validation pass
*before* it touches AIS or the junction matrices.  Two checks fire:

* **Close waypoints** -- distinct leg endpoints within 5% of the
  shortest incident leg's length.  The dialog zooms the canvas to
  the candidate and offers to snap to point 1, point 2, or the
  midpoint.  The snap rewrites every leg endpoint matching either
  source coordinate to the chosen target.
* **Crossing legs (X-intersections)** -- two legs that geometrically
  cross at an interior point (not at a shared endpoint).  The dialog
  asks whether to split each leg in two at the crossing point,
  producing four sub-legs that share a real junction node.  Sub-legs
  inherit the parent leg's traffic block.

After the validation pass, the junction registry is rebuilt against
the (possibly mutated) segment list, AIS update runs as before, and
junction matrices are refreshed -- AIS-derived where data are
available, geometry-derived where not, and user-edited rows are left
alone.


Editing a matrix manually
==========================

Open **Settings > Junction transition matrix...** to inspect or edit
the per-junction transition matrices.

.. figure:: _static/screenshots/ui_settings_junction_matrix.png
   :width: 90%
   :alt: The Junction transition matrix dialog showing the picker
         combo, source label, and percentage spinbox grid.

   The Junction transition matrix dialog.  The combo at the top picks
   one junction (labelled with the ``LEG_{route}_{leg}`` names of
   every leg meeting there); the **Source** label shows whether the
   matrix came from AIS, geometry heuristics, or a user edit.

The picker combo lists every junction in the project; each entry is
just the comma-separated leg names that meet at that point, so the
grid headers below it use the matching names.  The grid has rows for
inbound legs and columns for outbound legs; the diagonal is fixed
(a leg cannot transition to itself), and each cell is a percentage
spinbox.

Click **Save row** to commit your edits for the currently displayed
junction.  Values are normalised to sum to 1.0 on save (so you don't
have to type exact percentages), and the junction's ``source`` is
flipped to ``"user"`` so future *Update all distributions* runs leave
it alone.


JSON layout
===========

The ``junctions`` block of a saved ``.omrat`` file is keyed by
junction id (a stable string derived from the coordinate, e.g.
``"j_15.000000_55.000000"``):

.. code-block:: json

    {
      "junctions": {
        "j_15.000000_55.000000": {
          "point": [15.0, 55.0],
          "legs":  {"1": "end", "2": "start", "3": "start"},
          "transitions": {
            "1": {"2": 0.7, "3": 0.3},
            "2": {"1": 1.0},
            "3": {"1": 1.0}
          },
          "source": "user"
        }
      }
    }


Behind the scenes
=================

* Pure-geometry route validation:
  ``geometries/route_validation.py``
  (close-waypoint detection, X-intersection detection, snap, split).
* Junction registry + matrix derivation:
  ``geometries/junctions.py``.
* Plugin handler (live registry + load/save):
  ``omrat_utils/handle_junctions.py``.
* Validation-pass UI driver:
  ``omrat_utils/route_validation_ui.py``.
* Matrix editor dialog:
  ``omrat_utils/junction_matrix_dialog.py``.
* AIS-derived counts:
  ``compute/junction_transitions.py``
  (pure-Python normaliser) plus
  ``omrat_utils/handle_ais.py:compute_junction_transitions``
  (database query).
