Skip to content

Commit

Permalink
rider categories (#767)
Browse files Browse the repository at this point in the history
* rider categories

* wip

* wip
  • Loading branch information
felixguendling authored Mar 3, 2025
1 parent 9eaca63 commit 43775cf
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 56 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=d85db6fe4c8108084a4772cbf8b691c3f8d7faa0
commit=d569d2d5e7d1d05559880f608302d282bbf5058d
[cista]
[email protected]:felixguendling/cista.git
branch=master
Expand Down
4 changes: 2 additions & 2 deletions .pkg.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
5286413155307812813
7721497175139577109
cista e03a1ff0a84d3f638bf4bff7357d19e542640288
zlib-ng 68ab3e2d80253ec5dc3c83691d9ff70477b32cd3
boost 4a9aca6cb8af75be6e58f28c09cc7e39f61e6173
Expand Down Expand Up @@ -28,7 +28,7 @@ opentelemetry-cpp 60770dc9dc63e3543fc87d605b2e88fd53d7a414
pugixml 60175e80e2f5e97e027ac78f7e14c5acc009ce50
unordered_dense 2c7230ae7f9c30849a5b089fb4a5d11896b45dcf
wyhash 1e012b57fc2227a9e583a57e2eacb3da99816d99
nigiri d85db6fe4c8108084a4772cbf8b691c3f8d7faa0
nigiri d569d2d5e7d1d05559880f608302d282bbf5058d
conf f9bf4bd83bf55a2170725707e526cbacc45dcc66
expat 636c9861e8e7c119f3626d1e6c260603ab624516
libosmium 6e6d6b3081cc8bdf25dda89730e25c36eb995516
Expand Down
42 changes: 42 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1658,6 +1658,44 @@ components:
Index into the `Itinerary.fareTransfers[fareTransferIndex].effectiveFareLegProducts` array
to identify which effective fare leg this itinerary leg belongs to
RiderCategory:
type: object
required:
- riderCategoryName
- isDefaultFareCategory
properties:
riderCategoryName:
description: Rider category name as displayed to the rider.
type: string
isDefaultFareCategory:
description: Specifies if this category should be considered the default (i.e. the main category displayed to riders).
type: boolean
eligibilityUrl:
description: URL to a web page providing detailed information about the rider category and/or its eligibility criteria.
type: string

FareMediaType:
type: string
enum: [ "NONE", "PAPER_TICKET", "TRANSIT_CARD", "CONTACTLESS_EMV", "MOBILE_APP" ]
enumDescriptions:
NONE: No fare media involved (e.g., cash payment)
PAPER_TICKET: Physical paper ticket
TRANSIT_CARD: Physical transit card with stored value
CONTACTLESS_EMV: cEMV (contactless payment)
MOBILE_APP: Mobile app with virtual transit cards/passes

FareMedia:
type: object
required:
- fareMediaType
properties:
fareMediaName:
description: Name of the fare media. Required for transit cards and mobile apps.
type: string
fareMediaType:
description: The type of fare media.
$ref: '#/components/schemas/FareMediaType'

FareProduct:
type: object
required:
Expand All @@ -1674,6 +1712,10 @@ components:
currency:
description: ISO 4217 currency code. The currency of the cost of the fare product.
type: string
riderCategory:
$ref: '#/components/schemas/RiderCategory'
media:
$ref: '#/components/schemas/FareMedia'

FareTransferRule:
type: string
Expand Down
43 changes: 41 additions & 2 deletions src/journey_to_response.cc
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,52 @@ api::Itinerary journey_to_response(osr::ways const* w,

auto const fares =
with_fares ? std::optional{n::get_fares(tt, j)} : std::nullopt;
auto const to_fare_media_type =
[](n::fares::fare_media::fare_media_type const t) {
using fare_media_type = n::fares::fare_media::fare_media_type;
switch (t) {
case fare_media_type::kNone: return api::FareMediaTypeEnum::NONE;
case fare_media_type::kPaper:
return api::FareMediaTypeEnum::PAPER_TICKET;
case fare_media_type::kCard:
return api::FareMediaTypeEnum::TRANSIT_CARD;
case fare_media_type::kContactless:
return api::FareMediaTypeEnum::CONTACTLESS_EMV;
case fare_media_type::kApp: return api::FareMediaTypeEnum::MOBILE_APP;
}
std::unreachable();
};
auto const to_media = [&](n::fares::fare_media const& m) -> api::FareMedia {
return {.fareMediaName_ =
m.name_ == n::string_idx_t::invalid()
? std::nullopt
: std::optional{std::string{tt.strings_.get(m.name_)}},
.fareMediaType_ = to_fare_media_type(m.type_)};
};
auto const to_rider_category =
[&](n::fares::rider_category const& r) -> api::RiderCategory {
return {.riderCategoryName_ = std::string{tt.strings_.get(r.name_)},
.isDefaultFareCategory_ = r.is_default_fare_category_,
.eligibilityUrl_ = tt.strings_.try_get(r.eligibility_url_)
.and_then([](std::string_view s) {
return std::optional{std::string{s}};
})};
};
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_)}};
.currency_ = std::string{tt.strings_.get(p.currency_code_)},
.riderCategory_ =
p.rider_category_ == n::rider_category_idx_t::invalid()
? std::nullopt
: std::optional{to_rider_category(
f.rider_categories_[p.rider_category_])},
.media_ = p.media_ == n::fare_media_idx_t::invalid()
? std::nullopt
: std::optional{to_media(f.fare_media_[p.media_])}};
};
auto const to_rule = [](n::fares::fare_transfer_rule const& x) {
switch (x.fare_transfer_type_) {
Expand Down Expand Up @@ -159,7 +198,7 @@ api::Itinerary journey_to_response(osr::ways const* w,
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_);
r.fare_product_);
});
})};
})};
Expand Down
40 changes: 33 additions & 7 deletions ui/src/lib/ConnectionDetail.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import ArrowRight from 'lucide-svelte/icons/arrow-right';
import type { Itinerary, Leg } from '$lib/openapi';
import type { FareProduct, Itinerary, Leg } from '$lib/openapi';
import Time from '$lib/Time.svelte';
import { routeBorderColor, routeColor } from '$lib/modeStyle';
import { formatDurationSec, formatDistanceMeters } from '$lib/formatDuration';
Expand Down Expand Up @@ -78,6 +78,36 @@
</div>
{/snippet}

{#snippet productInfo(product: FareProduct)}
{product.name}
({product.amount}
{product.currency})
{#if product.riderCategory}
for
{#if product.riderCategory.eligibilityUrl}
<a
class:italic={product.riderCategory.isDefaultFareCategory}
class="underline"
href={product.riderCategory.eligibilityUrl}
>
{product.riderCategory.riderCategoryName}
</a>
{:else}
<span class:italic={product.riderCategory.isDefaultFareCategory}>
{product.riderCategory.riderCategoryName}
</span>
{/if}
{/if}
{#if product.media}
as
{#if product.media.fareMediaName}
{product.media.fareMediaName}
{:else}
{product.media.fareMediaType}
{/if}
{/if}
{/snippet}

{#snippet ticketInfo(prevTransitLeg: Leg | undefined, l: Leg)}
{#if itinerary.fareTransfers != undefined && l.fareTransferIndex != undefined && l.effectiveFareLegIndex != undefined}
{@const fareTransfer = itinerary.fareTransfers[l.fareTransferIndex]}
Expand All @@ -100,9 +130,7 @@
{#if productOptions.length == 1}
{t.ticket}
{/if}
{product.name}
({product.amount}
{product.currency})
{@render productInfo(product)}
</li>
{/each}
</ul>
Expand Down Expand Up @@ -141,9 +169,7 @@
<br />
<span class="text-xs font-bold text-foreground">
Ticket: {pred.effectiveFareLegIndex}
{transferProduct.name}
({transferProduct.amount}
{transferProduct.currency})
{@render productInfo(transferProduct)}
</span>
{/if}
{/if}
Expand Down
52 changes: 52 additions & 0 deletions ui/src/lib/openapi/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,52 @@ to identify which effective fare leg this itinerary leg belongs to
}
} as const;

export const RiderCategorySchema = {
type: 'object',
required: ['riderCategoryName', 'isDefaultFareCategory'],
properties: {
riderCategoryName: {
description: 'Rider category name as displayed to the rider.',
type: 'string'
},
isDefaultFareCategory: {
description: 'Specifies if this category should be considered the default (i.e. the main category displayed to riders).',
type: 'boolean'
},
eligibilityUrl: {
description: 'URL to a web page providing detailed information about the rider category and/or its eligibility criteria.',
type: 'string'
}
}
} as const;

export const FareMediaTypeSchema = {
type: 'string',
enum: ['NONE', 'PAPER_TICKET', 'TRANSIT_CARD', 'CONTACTLESS_EMV', 'MOBILE_APP'],
enumDescriptions: {
NONE: 'No fare media involved (e.g., cash payment)',
PAPER_TICKET: 'Physical paper ticket',
TRANSIT_CARD: 'Physical transit card with stored value',
CONTACTLESS_EMV: 'cEMV (contactless payment)',
MOBILE_APP: 'Mobile app with virtual transit cards/passes'
}
} as const;

export const FareMediaSchema = {
type: 'object',
required: ['fareMediaType'],
properties: {
fareMediaName: {
description: 'Name of the fare media. Required for transit cards and mobile apps.',
type: 'string'
},
fareMediaType: {
description: 'The type of fare media.',
'$ref': '#/components/schemas/FareMediaType'
}
}
} as const;

export const FareProductSchema = {
type: 'object',
required: ['name', 'amount', 'currency'],
Expand All @@ -634,6 +680,12 @@ export const FareProductSchema = {
currency: {
description: 'ISO 4217 currency code. The currency of the cost of the fare product.',
type: 'string'
},
riderCategory: {
'$ref': '#/components/schemas/RiderCategory'
},
media: {
'$ref': '#/components/schemas/FareMedia'
}
}
} as const;
Expand Down
Loading

0 comments on commit 43775cf

Please sign in to comment.