Skip to content

Commit

Permalink
GTFS Fares v2 (#766)
Browse files Browse the repository at this point in the history
* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* display leg fares TODO display transfer fares between legs

* wip
  • Loading branch information
felixguendling authored Mar 2, 2025
1 parent c2d06b1 commit 918ea60
Show file tree
Hide file tree
Showing 17 changed files with 427 additions and 101 deletions.
2 changes: 1 addition & 1 deletion .pkg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[nigiri]
[email protected]:motis-project/nigiri.git
branch=master
commit=4c925185fcabca0b3a154d5f5d2b212b536013ec
commit=d85db6fe4c8108084a4772cbf8b691c3f8d7faa0
[cista]
[email protected]:felixguendling/cista.git
branch=master
Expand Down
8 changes: 4 additions & 4 deletions .pkg.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
963138316830836792
5286413155307812813
cista e03a1ff0a84d3f638bf4bff7357d19e542640288
zlib-ng 68ab3e2d80253ec5dc3c83691d9ff70477b32cd3
boost 4a9aca6cb8af75be6e58f28c09cc7e39f61e6173
Expand All @@ -8,7 +8,7 @@ mimalloc e2f4fe647e8aff4603a7d5119b8639fd1a47c8a6
libressl 24acd9e710fbe842e863572da9d738715fbc74b8
docs 75dc89a53e9c2d78574fc0ffda698e69f1682ed2
fmt dc10f83be70ac2873d5f8d1ce317596f1fd318a2
utl 659d088d61718255547f83709f5cebc868c60708
utl afa4b1787c83f24fe7e11fa84902d3d51727e08b
res b759b93316afeb529b6cb5b2548b24c41e382fb0
date ce88cc33b5551f66655614eeebb7c5b7189025fb
yaml-cpp 1d8ca1f35eb3a9c9142462b28282a848e5d29a91
Expand All @@ -26,9 +26,9 @@ abseil-cpp ba5240842d352b4b67a32092453a2fe5fe53a62e
protobuf df2dd518c68b882c9dce5346393f8c388108e733
opentelemetry-cpp 60770dc9dc63e3543fc87d605b2e88fd53d7a414
pugixml 60175e80e2f5e97e027ac78f7e14c5acc009ce50
unordered_dense b33b037377ca966bbdd9cccc3417e46e88f83bfb
unordered_dense 2c7230ae7f9c30849a5b089fb4a5d11896b45dcf
wyhash 1e012b57fc2227a9e583a57e2eacb3da99816d99
nigiri 4c925185fcabca0b3a154d5f5d2b212b536013ec
nigiri d85db6fe4c8108084a4772cbf8b691c3f8d7faa0
conf f9bf4bd83bf55a2170725707e526cbacc45dcc66
expat 636c9861e8e7c119f3626d1e6c260603ab624516
libosmium 6e6d6b3081cc8bdf25dda89730e25c36eb995516
Expand Down
1 change: 1 addition & 0 deletions include/motis/journey_to_response.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ api::Itinerary journey_to_response(osr::ways const*,
street_routing_cache_t&,
osr::bitvec<osr::node_idx_t>& blocked_mem,
bool detailed_transfers,
bool with_fares,
double timetable_max_matching_distance,
double max_matching_distance);

Expand Down
89 changes: 88 additions & 1 deletion openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,14 @@ paths:
schema:
type: integer
minimum: 1

- name: withFares
in: query
required: false
description: Optional. Experimental. If set to true, the response will contain fare information.
schema:
type: boolean
default: false
responses:
'200':
description: routing result
Expand Down Expand Up @@ -1053,7 +1061,6 @@ components:
Duration:
description: Object containing duration if a path was found or none if no path was found
type: object
required: [ ]
properties:
duration:
type: number
Expand Down Expand Up @@ -1640,6 +1647,81 @@ components:
$ref: '#/components/schemas/StepInstruction'
rental:
$ref: '#/components/schemas/Rental'
fareTransferIndex:
type: integer
description: |
Index into `Itinerary.fareTransfers` array
to identify which fare transfer this leg belongs to
effectiveFareLegIndex:
type: integer
description: |
Index into the `Itinerary.fareTransfers[fareTransferIndex].effectiveFareLegProducts` array
to identify which effective fare leg this itinerary leg belongs to
FareProduct:
type: object
required:
- name
- amount
- currency
properties:
name:
description: The name of the fare product as displayed to riders.
type: string
amount:
description: The cost of the fare product. May be negative to represent transfer discounts. May be zero to represent a fare product that is free.
type: number
currency:
description: ISO 4217 currency code. The currency of the cost of the fare product.
type: string

FareTransferRule:
type: string
enum:
- A_AB
- A_AB_B
- AB

FareTransfer:
type: object
description: |
The concept is derived from: https://gtfs.org/documentation/schedule/reference/#fare_transfer_rulestxt
Terminology:
- **Leg**: An itinerary leg as described by the `Leg` type of this API description.
- **Effective Fare Leg**: Itinerary legs can be joined together to form one *effective fare leg*.
- **Fare Transfer**: A fare transfer groups two or more effective fare legs.
- **A** is the first *effective fare leg* of potentially multiple consecutive legs contained in a fare transfer
- **B** is any *effective fare leg* following the first *effective fare leg* in this transfer
- **AB** are all changes between *effective fare legs* contained in this transfer
The fare transfer rule is used to derive the final set of products of the itinerary legs contained in this transfer:
- A_AB means that any product from the first effective fare leg combined with the product attached to the transfer itself (AB) which can be empty (= free). Note that all subsequent effective fare leg products need to be ignored in this case.
- A_AB_B mean that a product for each effective fare leg needs to be purchased in a addition to the product attached to the transfer itself (AB) which can be empty (= free)
- AB only the transfer product itself has to be purchased. Note that all fare products attached to the contained effective fare legs need to be ignored in this case.
An itinerary `Leg` references the index of the fare transfer and the index of the effective fare leg in this transfer it belongs to.
required:
- effectiveFareLegProducts
properties:
rule:
$ref: '#/components/schemas/FareTransferRule'
transferProduct:
$ref: '#/components/schemas/FareProduct'
effectiveFareLegProducts:
description: |
Lists all valid fare products for the effective fare legs.
This is an `array<array<FareProduct>>` where the inner array
lists all possible fare products that would cover this effective fare leg.
Each "effective fare leg" can have multiple options for adult/child/weekly/monthly/day/one-way tickets etc.
You can see the outer array as AND (you need one ticket for each effective fare leg (`A_AB_B`), the first effective fare leg (`A_AB`) or no fare leg at all but only the transfer product (`AB`)
and the inner array as OR (you can choose which ticket to buy)
type: array
items:
type: array
items:
$ref: '#/components/schemas/FareProduct'

Itinerary:
type: object
Expand Down Expand Up @@ -1669,6 +1751,11 @@ components:
type: array
items:
$ref: '#/components/schemas/Leg'
fareTransfers:
description: Fare information
type: array
items:
$ref: '#/components/schemas/FareTransfer'

Footpath:
description: footpath from one location to another
Expand Down
1 change: 1 addition & 0 deletions src/endpoints/routing.cc
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ api::plan_response routing::operator()(boost::urls::url_view const& url) const {
query.pedestrianProfile_ ==
api::PedestrianProfileEnum::WHEELCHAIR,
j, start, dest, cache, *blocked, query.detailedTransfers_,
query.withFares_,
config_.timetable_
.and_then([](config::timetable const& x) {
return std::optional{x.max_matching_distance_};
Expand Down
2 changes: 1 addition & 1 deletion src/endpoints/trip.cc
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ api::Itinerary trip::operator()(boost::urls::url_view const& url) const {
.transfers_ = 0U},
tt_location{from_l.get_location_idx(),
from_l.get_scheduled_location_idx()},
tt_location{to_l.get_location_idx()}, cache, blocked, false,
tt_location{to_l.get_location_idx()}, cache, blocked, false, false,
config_.timetable_.value().max_matching_distance_, kMaxMatchingDistance);
}

Expand Down
94 changes: 88 additions & 6 deletions src/journey_to_response.cc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,33 @@ void cleanup_intermodal(api::Itinerary& i) {
}
}

struct fare_indices {
std::int64_t transfer_idx_;
std::int64_t effective_fare_leg_idx_;
};

std::optional<fare_indices> get_fare_indices(
std::optional<std::vector<n::fare_transfer>> const& fares,
n::routing::journey::leg const& l) {
if (!fares.has_value()) {
return std::nullopt;
}

for (auto const [transfer_idx, transfer] : utl::enumerate(*fares)) {
for (auto const [eff_fare_leg_idx, eff_fare_leg] :
utl::enumerate(transfer.legs_)) {
for (auto const* x : eff_fare_leg.joined_leg_) {
if (x == &l) {
return fare_indices{static_cast<std::int64_t>(transfer_idx),
static_cast<std::int64_t>(eff_fare_leg_idx)};
}
}
}
}

return std::nullopt;
}

api::Itinerary journey_to_response(osr::ways const* w,
osr::lookup const* l,
osr::platforms const* pl,
Expand All @@ -72,22 +99,71 @@ api::Itinerary journey_to_response(osr::ways const* w,
street_routing_cache_t& cache,
osr::bitvec<osr::node_idx_t>& blocked_mem,
bool const detailed_transfers,
bool const with_fares,
double const timetable_max_matching_distance,
double const max_matching_distance) {
utl::verify(!j.legs_.empty(), "journey without legs");

auto const fares =
with_fares ? std::optional{n::get_fares(tt, j)} : std::nullopt;
auto const to_product =
[&](n::fares const& f,
n::fare_product_idx_t const x) -> api::FareProduct {
auto const& p = f.fare_products_[x];
return {.name_ = std::string{tt.strings_.get(p.name_)},
.amount_ = p.amount_,
.currency_ = std::string{tt.strings_.get(p.currency_code_)}};
};
auto const to_rule = [](n::fares::fare_transfer_rule const& x) {
switch (x.fare_transfer_type_) {
case nigiri::fares::fare_transfer_rule::fare_transfer_type::kAB:
return api::FareTransferRuleEnum::AB;
case nigiri::fares::fare_transfer_rule::fare_transfer_type::kAPlusAB:
return api::FareTransferRuleEnum::A_AB;
case nigiri::fares::fare_transfer_rule::fare_transfer_type::kAPlusABPlusB:
return api::FareTransferRuleEnum::A_AB_B;
}
std::unreachable();
};

auto itinerary = api::Itinerary{
.duration_ = to_seconds(j.arrival_time() - j.departure_time()),
.startTime_ = j.legs_.front().dep_time_,
.endTime_ = j.legs_.back().arr_time_,
.transfers_ = std::max(
static_cast<std::iterator_traits<
decltype(j.legs_)::iterator>::difference_type>(0),
utl::count_if(j.legs_, [](auto&& leg) {
return holds_alternative<n::routing::journey::run_enter_exit>(
leg.uses_) ||
odm::is_odm_leg(leg);
}) - 1)};
utl::count_if(
j.legs_,
[](auto&& leg) {
return holds_alternative<n::routing::journey::run_enter_exit>(
leg.uses_) ||
odm::is_odm_leg(leg);
}) -
1),
.fareTransfers_ =
fares.and_then([&](std::vector<n::fare_transfer> const& transfers) {
return std::optional{utl::to_vec(
transfers, [&](n::fare_transfer const& t) -> api::FareTransfer {
return {.rule_ = t.rule_.and_then([&](auto&& r) {
return std::optional{to_rule(r)};
}),
.transferProduct_ = t.rule_.and_then([&](auto&& r) {
return t.legs_.empty()
? std::nullopt
: std::optional{to_product(
tt.fares_[t.legs_.front().src_],
r.fare_product_)};
}),
.effectiveFareLegProducts_ =
utl::to_vec(t.legs_, [&](auto&& l) {
return utl::to_vec(l.rule_, [&](auto&& r) {
return to_product(tt.fares_[l.src_],
r.fare_product_id_);
});
})};
})};
})};

auto const append = [&](api::Itinerary&& x) {
itinerary.legs_.insert(end(itinerary.legs_),
Expand Down Expand Up @@ -118,6 +194,7 @@ api::Itinerary journey_to_response(osr::ways const* w,
auto const exit_stop = fr[t.stop_range_.to_ - 1U];
auto const color = enter_stop.get_route_color();
auto const agency = enter_stop.get_provider();
auto const fare_indices = get_fare_indices(fares, j_leg);

auto& leg = itinerary.legs_.emplace_back(api::Leg{
.mode_ = to_mode(enter_stop.get_clasz()),
Expand All @@ -142,7 +219,12 @@ api::Itinerary journey_to_response(osr::ways const* w,
.tripId_ = tags.id(tt, enter_stop, n::event_type::kDep),
.routeShortName_ = {std::string{
enter_stop.trip_display_name()}},
.source_ = fmt::to_string(fr.dbg())});
.source_ = fmt::to_string(fr.dbg()),
.fareTransferIndex_ = fare_indices.and_then(
[](auto&& x) { return std::optional{x.transfer_idx_}; }),
.effectiveFareLegIndex_ = fare_indices.and_then([](auto&& x) {
return std::optional{x.effective_fare_leg_idx_};
})});
leg.from_.vertexType_ = api::VertexTypeEnum::TRANSIT;
leg.from_.departure_ = leg.startTime_;
leg.from_.scheduledDeparture_ = leg.scheduledStartTime_;
Expand Down
2 changes: 1 addition & 1 deletion src/odm/meta_router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ api::plan_response meta_router::run() {
query_.pedestrianProfile_ ==
api::PedestrianProfileEnum::WHEELCHAIR,
j, start_, dest_, cache, *ep::blocked,
query_.detailedTransfers_,
query_.detailedTransfers_, query_.withFares_,
r_.config_.timetable_->max_matching_distance_,
query_.maxMatchingDistance_);
}),
Expand Down
Loading

0 comments on commit 918ea60

Please sign in to comment.