Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GTFS Fares v2 #766

Merged
merged 10 commits into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading