Skip to content

feat: respect GTFS transfers.txt min_transfer_time in footpath generation #339

Description

@bveluetch

Problem

When building footpaths, connect_components() in build_footpaths.cc clears all preprocessing footpaths (line 209) and recomputes them via Dijkstra. This erases the GTFS transfers.txt min_transfer_time values that were correctly parsed and stored in read_transfers() (stop.cc).

As a result, all inter-stop transfer times are computed solely from walking distance, ignoring operator-provided minimum transfer times. For example, at Paris Gare de Lyon, IDFM specifies 6-8 minutes between metro platforms and mainline platforms in their transfers.txt (206,187 entries), but MOTIS returns 0 minutes.

Root Cause Analysis

In src/loader/gtfs/stop.cc, read_transfers() correctly parses transfer_type=2 entries and stores them as footpaths (lines 192-200):

auto& footpaths = from_stop_it->second->footpaths_;
footpaths.emplace_back(to_stop_it->second.get(), duration_t{*t.min_transfer_time_ / 60});

These are written to preprocessing_footpaths_out_ (lines 347-352).

However, connect_components() in build_footpaths.cc then:

  1. Clears all preprocessing footpaths (line 209): tt.locations_.preprocessing_footpaths_out_.clear()
  2. Recomputes everything via Dijkstra using only walking distances
  3. The GTFS min_transfer_time values are lost

Proposed Fix

In connect_components(), before clearing the preprocessing footpaths, save the GTFS-provided footpath durations. Then after the Dijkstra computation, ensure the final duration is at least the GTFS minimum:

// Before clear: save GTFS footpath durations
hash_map<std::pair<location_idx_t, location_idx_t>, duration_t> gtfs_min_durations;
for (auto l = location_idx_t{0U}; l != tt.locations_.preprocessing_footpaths_out_.size(); ++l) {
  for (auto const& fp : tt.locations_.preprocessing_footpaths_out_[l]) {
    auto const key = std::make_pair(l, fp.target());
    auto it = gtfs_min_durations.find(key);
    if (it == gtfs_min_durations.end() || fp.duration() > it->second) {
      gtfs_min_durations[key] = fp.duration();
    }
  }
}

// ... existing clear + Dijkstra code ...

// When writing footpaths (around line 294), add:
auto const gtfs_it = gtfs_min_durations.find({from_l, to_l});
auto const gtfs_min = (gtfs_it != gtfs_min_durations.end())
    ? gtfs_it->second : duration_t{0};
auto const duration = std::max({
    std::chrono::duration_cast<u8_minutes>(fp.duration()),
    tt.locations_.transfer_time_[from_l],
    tt.locations_.transfer_time_[to_l],
    gtfs_min  // NEW: respect GTFS min_transfer_time
});

This ensures the computed footpath is at least as long as the GTFS-specified minimum, while still allowing the Dijkstra-computed time to be longer if the walking distance warrants it.

Additionally: respect transfer_type=3

Currently, transfer_type=3 (not possible) entries are simply skipped (line 150-152). They should also prevent footpath creation between those stop pairs, even if they are geographically close.

Impact

This would fix:

Operators like IDFM (Ile-de-France Mobilites) publish detailed transfer times in their GTFS that account for platform layout, elevators, corridors, and fare gates - information that pure walking distance cannot capture.

Test Case

A minimal GTFS with two stops at 50m distance and a transfers.txt entry with min_transfer_time=600 (10 min) should produce a footpath of 10 min, not the ~1 min computed from walking distance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions