# Lane-Changing Behavior — How It Works

## For the Casual Reader

When you watch the simulation, every car is constantly making small decisions about which lane to be in. A car heading toward a left turn will gradually work its way toward the left-hand lane before it reaches the intersection. A car heading straight will pick any available lane. When a light turns green, a car glides smoothly across the intersection onto the next road.

These movements are not pre-planned. Each car only knows what the **next intersection** will be. It has no map of the whole city and no idea what comes after. At each intersection it randomly picks a direction, then asks: *"Which lane do I need to be in to make that turn?"* The rest — the smooth curve, the timing, the braking at red lights — all follows from that one local decision.

There are two kinds of lane changes in this simulation:

1. **Repositioning** — sliding sideways within the same road to reach the preferred lane for an upcoming turn.
2. **Crossing** — the sweeping curve a car makes as it travels through an intersection from one road to another.

Both look similar on screen (a smooth curved path), but they are triggered and governed by different rules.

---

## The Two Types of Lane Changes

### Type 1 — Repositioning (Same Road)

A car that has picked a left turn for its next intersection needs to be in the **leftmost lane** before it gets there. If it is not already there, it will try to shift over as it drives down the road.

This kind of change:
- Only ever moves one lane at a time (left or right neighbor).
- Is blocked if the car is too close to the end of the road (less than 3 car-lengths of clearance before the stop line).
- Is also blocked if the car is already mid-crossing (one lane change at a time).
- Is **safety-gated** by `Car._isTurnRepositionSafe(targetLane)`: if the gap to the nearest car behind in the target lane is smaller than `s0 + timeHeadway × follower.speed`, the move is aborted and retried next tick.

### Type 2 — Crossing (Across an Intersection)

When a car reaches the stop line and the traffic light permits its intended turn, it begins a crossing maneuver. It leaves its approach lane, sweeps through the intersection along a smooth Bézier curve, and arrives in the destination lane on the other side.

This kind of change:
- Is gated by the traffic signal (no crossing on red).
- Is also blocked if the intended turn violates the rules (no U-turns; left turns only from the leftmost lane; right turns only from the rightmost lane).
- Commits the car to a new road — there is no cancelling mid-crossing.

---

## How a Car Picks Its Next Lane

Every car carries a `nextLane` reference — the lane it is trying to reach on the **next** road after the upcoming intersection. This is picked by `Car.pickNextLane()`, which is called automatically whenever a car finishes a crossing and does not yet have a destination.

```
pickNextLane() steps:
  1. Ask the upcoming intersection for all outgoing roads
     (excluding the road the car just came from — no U-turns at map level)
  2. Choose one at random → this is nextRoad
  3. Call _peekSecondAheadBias(nextRoad) → store result in car.turnBias ('left'/'right'/null)
     (samples a random outgoing road from nextRoad's target intersection and checks its turn direction;
      used later by _evalMobil() to apply an anticipatory positioning bonus)
  4. Compute the turn type: left (0), forward (1), right (2)
  5. Get currentIndex = position of current lane in its road's lanes array (0 = rightmost)
  6. Pick the lane within nextRoad using position-preserving mapping:
       left  turn  → preserve offset from the LEFT  edge:
                     indexFromLeft = (srcLanes - 1) - currentIndex
                     laneNumber    = max(0, dstLanes - 1 - indexFromLeft)
       forward     → preserve lane index, clamped to destination count:
                     laneNumber    = min(currentIndex, dstLanes - 1)
       right turn  → preserve offset from the RIGHT edge:
                     laneNumber    = min(currentIndex, dstLanes - 1)
  7. Store as car.nextLane
```

The position-preserving rule ensures two cars side-by-side on the approach road always arrive side-by-side on the exit road, with no crossing Bézier paths inside the intersection. (Previously, forward traffic used a random lane, which could produce directly crossing paths.)

No global path planning exists. The car picks its next turn at random, every intersection, for its entire lifetime.

---

## Repositioning: Getting Into the Right Lane

Once `nextLane` is known, the car tries to work its way toward the preferred lane **within its current road**. This logic runs every physics tick in `Car.move()`.

```
Every tick (if not already mid-lane-change and nextLane is set):
  1. Compute the turn type from currentLane to nextLane
  2. If left turn  → preferred = currentLane.leftmostAdjacent
     If right turn → preferred = currentLane.rightmostAdjacent
     If forward    → preferred = currentLane (no move needed)
  3. If preferred ≠ current lane → attempt trajectory.changeLane(preferred)
     (throws silently if not possible right now)
```

Because the car only moves one lane per tick at most, and only one lane change can be active at a time, a car on a multi-lane road may need several successive lane changes to reach the leftmost lane. Each change takes time. If the road is short and the car runs out of room, it may miss its preferred lane and still attempt the crossing from wherever it ends up — this is allowed as long as the turn is still valid from that lane.

---

## The Crossing: Geometry of the Bézier Curve

Both repositioning and crossing maneuvers travel along a **cubic Bézier curve**. This is what gives the motion its smooth, natural-looking arc.

### What Is a Bézier Curve?

A cubic Bézier curve is defined by four points: two endpoints (where the car starts and ends) and two control points (which shape how the arc bends). The car never touches the control points — they act like magnets pulling the curve into shape.

This simulation uses **De Casteljau's algorithm** to evaluate the curve: given a parameter `a` from 0 (start) to 1 (end), it repeatedly interpolates between pairs of points until only one point remains. That point is the car's position.

```
Curve(A, B, O, Q):
  A = start position    O = control point near A
  B = end position      Q = control point near B

getPoint(a):
  p0 = lerp(A, O, a)        ← on segment A→O
  p1 = lerp(O, Q, a)        ← on segment O→Q
  p2 = lerp(Q, B, a)        ← on segment Q→B
  r0 = lerp(p0, p1, a)
  r1 = lerp(p1, p2, a)
  return lerp(r0, r1, a)    ← final position
```

### How the Control Points Are Chosen

The control points are derived from the **travel directions** of the two lanes involved. This ensures the curve enters and exits tangent to the road — the car does not abruptly change heading at the start or end of the maneuver.

```
_getAdjacentLaneChangeCurve():
  p1 = current position in current lane
  p2 = target position in destination lane
  distance = |p2 - p1|

  control1 = p1 + (current lane direction) × 0.3 × distance
  control2 = p2 - (destination lane direction) × 0.3 × distance

  return Curve(p1, p2, control1, control2)
```

The `0.3 × distance` offset means the control handles are 30% of the arc length, which produces a smooth "S" shape for adjacent lane changes and a broad sweep for intersection crossings.

### Arc Length

The curve's length cannot be computed in closed form, so it is approximated by sampling 10 evenly-spaced points along the curve and summing the chord lengths. This value is computed once and cached — it is used to synchronize the car's position in the destination lane as the crossing proceeds.

---

## The State Machine: Three-Position Slots

This is the most important implementation concept. Each car's `Trajectory` object tracks the car's position across **three simultaneous lane slots**:

| Slot | Name | Role |
|---|---|---|
| `current` | Current lane | The lane the car is in when not changing lanes. Drives rendering when `temp` is empty. |
| `next` | Destination lane | The lane the car is heading toward during a change. |
| `temp` | Curve lane | A `Curve` object acting as a temporary "lane" during the animation. Drives rendering while active. |

At any moment, only one slot drives where the car appears on screen: `temp` if it is set, otherwise `current`.

### Phase Transitions

**Phase 1 — Normal driving**
Only `current` is active and registered in its lane. `next` and `temp` are empty.

```
current: [lane=A, position=p, registered=YES]
next:    [empty]
temp:    [empty]
```

**Phase 2 — Initiate** (`_startChangingLanes`)

A Bézier curve is constructed from the current position to the target position. `temp` is set to this curve. `next` is pointed at the destination lane with a **negative** starting position — it will count up to zero and beyond as the curve progresses, so that when the car finishes the curve, `next.position` is the car's position in the new lane.

```
current: [lane=A, position=p, registered=YES]
next:    [lane=B, position=(0 - curveLength), registered=NO]
temp:    [lane=Curve, position=0, registered=NO]
isChangingLanes = true
```

**Phase 3 — Executing** (inside `moveForward` each tick)

All three positions advance by the same distance. `temp` drives rendering — the car appears to follow the curve smoothly. Two sub-events happen automatically:

- **Release current** when `temp.position > 2 × carLength`: the car is far enough into the curve that it no longer "occupies" the old lane for following-distance purposes.
- **Acquire next** when `temp.position + 2 × carLength > curveLength`: the car is close enough to the end of the curve that it needs to start occupying the destination lane so cars already there will respond to it. Before acquiring, `_nextAcquireIsSafe()` checks that the nearest follower in the destination lane would not be forced to brake harder than `Settings.mobil.safeDecel`. If unsafe, acquisition is delayed one tick; an override forces it within the final `car.length` of the curve to prevent indefinite stalls.

```
(mid-crossing)
current: [lane=A, position=p+d, registered=NO]   ← released
next:    [lane=B, position=negative→positive, registered=YES]  ← acquired near end
temp:    [lane=Curve, position=0→curveLength, registered=NO]
```

**Phase 4 — Finish** (`_finishChangingLanes`)

When `temp.position / curveLength >= 1` the crossing is complete. `next` is promoted to `current`. `temp` and the old `next` are cleared.

```
current: [lane=B, position=q, registered=YES]   ← promoted from next
next:    [empty]
temp:    [empty]
isChangingLanes = false
```

---

## Traffic Signal Integration

When a car is approaching an intersection at red, a braking force is applied via the IDM acceleration model. The key value is `distanceToStopLine`:

```
distanceToStopLine:
  if canEnterIntersection() → return Infinity   (no braking needed, green light)
  else                      → return _getDistanceToIntersection()
```

`_getDistanceToIntersection()` returns the physical distance from the car's front bumper to the end of the lane, **except** during an intersection crossing (when `isChangingLanes` is true and `next._lane` is on a different road) — in that case it also returns `Infinity` because the car has already been granted entry and must not brake mid-crossing.

`canEnterIntersection()` looks up the signal state for the car's approach side and intended turn type, with a second path for right-turn-on-red:

```
canEnterIntersection():
  if no nextLane → true (car has nowhere to go, let it reach the line)
  turnNumber = sourceLane.getTurnDirection(nextLane)     ← 0/1/2 = left/fwd/right
  sideId     = sourceLane.road.targetSideId              ← which side of intersection

  if signals.state[sideId][turnNumber] === 1 → true     (green light)

  // Right-turn-on-red: allow if stopped and target lane entry is clear
  if Settings.rightTurnOnRed
     && turnNumber === 2
     && car.speed < Settings.rightTurnOnRedMaxSpeed:
       nearest  = nextLane.getNextAt(0)
       entryGap = nearest ? nearest.position - nearest.car.length/2 : Infinity
       if entryGap > Settings.rightTurnOnRedMinGap → true

  return false
```

The RTOR gap check uses `Lane.getNextAt(0)` — the same MOBIL-style query — to find the nearest registered car at the entry of the target lane. Cars mid-crossing the intersection on a Bézier curve are not yet registered and are therefore invisible to this check; IDM handles the merge once they acquire the destination lane slot.

---

## Car Following During a Lane Change

The IDM model needs to know the distance to the next car ahead. During a crossing, a car is registered in **two** lanes simultaneously (the destination lane once acquired, and the old lane until released). `Trajectory.nextCarDistance` takes the smaller of both:

```
nextCarDistance:
  a = current.nextCarDistance    ← nearest car ahead in old lane
  b = next.nextCarDistance       ← nearest car ahead in destination lane
  return whichever distance is smaller
```

This means a car crossing into a lane where a slow car is already present will automatically slow down. It also means a car in the destination lane "sees" the arriving car as soon as it acquires the slot, and will brake accordingly.

---

## Lane Adjacency

`Road.update()` builds the adjacency graph for all lanes when a road is created or modified. Lane indexing: `lanes[0]` is the **rightmost** lane; `lanes[lanesNumber-1]` is the **leftmost**.

```
For each lane i:
  leftAdjacent      = lanes[i + 1]         (null if already leftmost)
  rightAdjacent     = lanes[i - 1]         (null if already rightmost)
  leftmostAdjacent  = lanes[lanesNumber-1] (always the leftmost, for any lane)
  rightmostAdjacent = lanes[0]             (always the rightmost, for any lane)
```

`isLeftmost` and `isRightmost` are simply `this === this.leftmostAdjacent` and `this === this.rightmostAdjacent`. These are used by `isValidTurn()` to enforce that left turns can only be made from the leftmost lane and right turns from the rightmost.

---

## Constraints and Validation

### `isValidTurn()`
Called before initiating any intersection crossing. Rejects:
- U-turns (`turnNumber === 3`)
- Left turns from any lane that is not the leftmost
- Right turns from any lane that is not the rightmost

### `changeLane()` guards
Called for voluntary adjacent repositioning. Throws (caught silently) if:
- `isChangingLanes` — already mid lane change
- `nextLane` is null or the same as the current lane
- `nextLane.road !== currentLane.road` — not a neighbor on the same road
- `currentPosition + 3 × carLength >= laneLength` — too close to the intersection to complete safely

### `moveForward()` crossing guards
```
if timeToMakeTurn() && canEnterIntersection() && isValidTurn():
  → start crossing
```
All three conditions must be true simultaneously.

---

## Known Limitations

- **✓ Safety-gated repositioning.** `Car._isTurnRepositionSafe(targetLane)` now aborts turn-lane moves when the gap to the following car in the target lane is below the IDM desired following distance.

- **One lane change at a time.** A car on a 4-lane road that needs the leftmost lane must execute successive single-step moves. If the road is short, it may not have time to complete all of them.

- **✓ Anticipatory look-ahead (one level deep).** `pickNextLane()` now peeks at the second intersection ahead via `_peekSecondAheadBias()` and stores the expected turn side in `car.turnBias`. MOBIL applies a bonus when moving toward that side, allowing cars to pre-position laterally before reaching the immediate intersection.

- **Random routing only.** There is no global path planning. Routing is purely local and random at each intersection. A car has no concept of a destination.

- **✓ Yield gate on merge.** `Trajectory._nextAcquireIsSafe()` delays `next.acquire()` when acquiring would force the nearest destination-lane follower into braking harder than `Settings.mobil.safeDecel`. An override prevents indefinite stalls.

- **No multi-step repositioning on very short roads.** The `changeLane()` guard rejects changes within `3 × carLength` of the stop line. On short roads, a car already in the wrong lane may simply fail all its change attempts and cross from whatever lane it is in (as long as `isValidTurn()` still permits it).

---

## Enhancement Opportunities

### 1. ✓ Safety Gap Check Before Repositioning *(implemented)*

`Car._isTurnRepositionSafe(targetLane)` checks the gap between the subject car and the nearest car behind it in the target lane. If `gap < s0 + timeHeadway × follower.speed`, the move is aborted and retried next tick. All turn-lane repositioning moves in `Car.move()` are gated by this check.

---

### 2. ✓ MOBIL-Style Lane Change Decisions *(implemented)*

Cars opportunistically change lanes using the MOBIL (Minimizing Overall Braking Induced by Lane Changes) model. A change is approved when (a) the car itself gains acceleration, (b) the new follower in the target lane is not forced to brake harder than `Settings.mobil.safeDecel`, and (c) the weighted net gain across all affected drivers exceeds `Settings.mobil.threshold`. Implemented in `Car._evalMobil()`. See [mobil.md](mobil.md) for full details.

---

### 3. ✓ Anticipatory Positioning (Look Two Intersections Ahead) *(implemented)*

`Car._peekSecondAheadBias(nextRoad)` samples a random outgoing road from `nextRoad.target` and determines whether it would require a left or right turn. The result is stored in `car.turnBias`. `_evalMobil()` applies `+Settings.mobil.anticipatoryBias` when a MOBIL move is toward the `turnBias` side. This stacks with the keep-right bias, allowing a genuine upcoming left turn to overcome the rightward drift pressure.

---

### 4. ✓ Yielding / Merge Priority *(implemented)*

`Trajectory._nextAcquireIsSafe()` runs just before `next.acquire()` inside `moveForward()`. It finds the nearest follower in the destination lane at `next.position` and calls `follower.car.getAccelerationGiven(follower.car.speed, gap, car.speed)`. If the result is below `−Settings.mobil.safeDecel`, acquisition is skipped for one tick. The car is forced to acquire when the remaining curve distance is ≤ `car.length` to prevent an indefinite stall if the destination lane never clears.

---

### 5. Multi-Step Repositioning

**What the code does now:** A car attempts one adjacent lane change per tick (throttled by `isChangingLanes`). If it needs to cross two or more lanes (e.g., middle lane to leftmost on a 4-lane road) it must do so in successive changes.

**What to add:** Queue a target lane (`targetLane`) distinct from the immediate step (`nextStep`). After each single-lane change finishes, automatically initiate the next step if `targetLane` has not been reached. This ensures multi-lane repositioning completes without the car having to re-evaluate each tick.

**Where:** New `pendingLaneTarget` property on `Car` or `Trajectory`; checked in `_finishChangingLanes()`.

---

### 6. ✓ Visual Turn Indicators *(implemented)*

Cars display a blinking amber side stripe when they have a left or right turn intention. The stripe blinks at ~1 Hz using `Math.floor(currentFrameTime / 500) % 2`. The `car.turnSignal` property (`'left'`, `'right'`, or `null`) is set each tick by `Car._updateTurnSignal()` and read by `Visualizer.drawCar()` to render the stripe at 1/3 car width, flush to the relevant edge.

---

### 7. Ramp Merge Behavior

**What the code does now:** The beltway's single ramp on/off point feeds into a standard intersection crossing. Cars on the ramp do not behave differently from any other car.

**What to add:** For ramp-to-mainline merges, enforce that cars on the ramp must yield to through traffic. This would require marking certain roads as "ramp" type and giving mainline cars priority in the `canEnterIntersection()` check.

**Where:** A new `road.isRamp` boolean; extended signal logic in `ControlSignals` or a new `RampSignal` class.

---

## Quick Reference: Key Files and Methods

| Concern | File | Method / Property |
|---|---|---|
| Pick next road and lane | [js/model/Car.js](js/model/Car.js) | `pickNextRoad()`, `pickNextLane()` |
| Peek second intersection ahead | [js/model/Car.js](js/model/Car.js) | `_peekSecondAheadBias()` |
| Turn-reposition safety check | [js/model/Car.js](js/model/Car.js) | `_isTurnRepositionSafe()` |
| Attempt repositioning | [js/model/Car.js](js/model/Car.js) | `move()` |
| Guard: too close to intersection | [js/model/Trajectory.js](js/model/Trajectory.js) | `changeLane()` |
| Guard: turn legality | [js/model/Trajectory.js](js/model/Trajectory.js) | `isValidTurn()` |
| Build Bézier curve | [js/model/Trajectory.js](js/model/Trajectory.js) | `_getAdjacentLaneChangeCurve()` |
| Initiate lane change | [js/model/Trajectory.js](js/model/Trajectory.js) | `_startChangingLanes()` |
| Advance positions, release/acquire | [js/model/Trajectory.js](js/model/Trajectory.js) | `moveForward()` |
| Yield gate at next.acquire | [js/model/Trajectory.js](js/model/Trajectory.js) | `_nextAcquireIsSafe()` |
| Complete lane change | [js/model/Trajectory.js](js/model/Trajectory.js) | `_finishChangingLanes()` |
| Bézier curve math | [js/geom/Curve.js](js/geom/Curve.js) | `getPoint()`, `getDirection()`, `length` |
| Lane adjacency graph | [js/model/Road.js](js/model/Road.js) | `update()` |
| Register/unregister in lane | [js/model/LanePosition.js](js/model/LanePosition.js) | `acquire()`, `release()` |
| Find nearest car ahead | [js/model/Lane.js](js/model/Lane.js) | `getNext()` |
| Signal gate | [js/model/Trajectory.js](js/model/Trajectory.js) | `canEnterIntersection()` |
| Stop-line braking distance | [js/model/Trajectory.js](js/model/Trajectory.js) | `distanceToStopLine`, `_getDistanceToIntersection()` |
| Render crossing curve (debug) | [js/visualizer/Visualizer.js](js/visualizer/Visualizer.js) | `drawCar()` debug block |
