1use std::{
2 collections::BTreeSet,
3 hash::{DefaultHasher, Hash, Hasher},
4};
5
6use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
7use rustc_hash::FxHashMap;
8use serde::{Deserialize, Serialize};
9use strum_macros::{self, Display, EnumString};
10
11use thiserror::Error;
12
13use crate::{
14 error::{HResult, HrdfError},
15 storage::DataStorage,
16 utils::{add_1_day, sub_1_day},
17};
18
19pub(crate) type JourneyId = (i32, String); pub trait Model<M: Model<M>> {
26 type K: Copy + Eq + Hash + Serialize + for<'a> Deserialize<'a>;
28
29 fn id(&self) -> M::K;
30}
31
32macro_rules! impl_Model {
33 ($m:ty) => {
34 impl Model<$m> for $m {
35 type K = i32;
36
37 fn id(&self) -> Self::K {
38 self.id
39 }
40 }
41 };
42}
43
44#[derive(Debug, Serialize, Deserialize)]
49pub struct Attribute {
50 id: i32,
51 designation: String,
52 stop_scope: i16,
53 main_sorting_priority: i16,
54 secondary_sorting_priority: i16,
55 description: FxHashMap<Language, String>,
56}
57
58impl_Model!(Attribute);
59
60impl Attribute {
61 pub fn new(
62 id: i32,
63 designation: String,
64 stop_scope: i16,
65 main_sorting_priority: i16,
66 secondary_sorting_priority: i16,
67 ) -> Self {
68 Self {
69 id,
70 designation,
71 stop_scope,
72 main_sorting_priority,
73 secondary_sorting_priority,
74 description: FxHashMap::default(),
75 }
76 }
77
78 pub fn set_description(&mut self, language: Language, value: &str) {
81 self.description.insert(language, value.to_string());
82 }
83}
84
85#[derive(Debug, Serialize, Deserialize)]
90pub struct BitField {
91 id: i32,
92 bits: Vec<u8>,
93}
94
95impl_Model!(BitField);
96
97impl BitField {
98 pub fn new(id: i32, bits: Vec<u8>) -> Self {
99 Self { id, bits }
100 }
101
102 pub fn bits(&self) -> &Vec<u8> {
105 &self.bits
106 }
107}
108
109#[derive(Debug, Default, Serialize, Deserialize)]
114pub struct Color {
115 r: i16,
116 g: i16,
117 b: i16,
118}
119
120#[allow(unused)]
121impl Color {
122 pub fn new(r: i16, g: i16, b: i16) -> Self {
123 Self { r, g, b }
124 }
125
126 pub fn r(&self) -> i16 {
129 self.r
130 }
131
132 pub fn g(&self) -> i16 {
133 self.g
134 }
135
136 pub fn b(&self) -> i16 {
137 self.b
138 }
139}
140
141#[derive(Clone, Copy, Debug, Default, Display, Eq, Hash, PartialEq, Serialize, Deserialize)]
146pub enum CoordinateSystem {
147 #[default]
148 LV95,
149 WGS84,
150}
151
152#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
157pub struct Coordinates {
158 coordinate_system: CoordinateSystem,
159 x: f64,
160 y: f64,
161}
162
163#[allow(unused)]
164impl Coordinates {
165 pub fn new(coordinate_system: CoordinateSystem, x: f64, y: f64) -> Self {
166 Self {
167 coordinate_system,
168 x,
169 y,
170 }
171 }
172
173 pub fn easting(&self) -> Option<f64> {
176 match self.coordinate_system {
177 CoordinateSystem::LV95 => Some(self.x),
178 CoordinateSystem::WGS84 => None,
179 }
180 }
181
182 pub fn northing(&self) -> Option<f64> {
183 match self.coordinate_system {
184 CoordinateSystem::LV95 => Some(self.y),
185 CoordinateSystem::WGS84 => None,
186 }
187 }
188
189 pub fn latitude(&self) -> Option<f64> {
190 match self.coordinate_system {
191 CoordinateSystem::WGS84 => Some(self.x),
192 CoordinateSystem::LV95 => None,
193 }
194 }
195
196 pub fn longitude(&self) -> Option<f64> {
197 match self.coordinate_system {
198 CoordinateSystem::WGS84 => Some(self.y),
199 CoordinateSystem::LV95 => None,
200 }
201 }
202}
203
204#[derive(Debug, Serialize, Deserialize)]
209pub struct Direction {
210 id: i32,
211 name: String,
212}
213
214impl_Model!(Direction);
215
216impl Direction {
217 pub fn new(id: i32, name: String) -> Self {
218 Self { id, name }
219 }
220}
221
222#[derive(
227 Clone, Copy, Debug, Default, Display, Eq, Hash, PartialEq, EnumString, Serialize, Deserialize,
228)]
229pub enum DirectionType {
230 #[default]
231 #[strum(serialize = "R")]
232 Outbound,
233
234 #[strum(serialize = "H")]
235 Return,
236}
237
238#[derive(Debug, Serialize, Deserialize)]
243pub struct Holiday {
244 id: i32,
245 date: NaiveDate,
246 name: FxHashMap<Language, String>,
247}
248
249impl_Model!(Holiday);
250
251impl Holiday {
252 pub fn new(id: i32, date: NaiveDate, name: FxHashMap<Language, String>) -> Self {
253 Self { id, date, name }
254 }
255}
256
257#[derive(Debug, Serialize, Deserialize)]
262pub struct ExchangeTimeAdministration {
263 id: i32,
264 stop_id: Option<i32>, administration_1: String,
266 administration_2: String,
267 duration: i16, }
269
270impl_Model!(ExchangeTimeAdministration);
271
272impl ExchangeTimeAdministration {
273 pub fn new(
274 id: i32,
275 stop_id: Option<i32>,
276 administration_1: String,
277 administration_2: String,
278 duration: i16,
279 ) -> Self {
280 Self {
281 id,
282 stop_id,
283 administration_1,
284 administration_2,
285 duration,
286 }
287 }
288
289 pub fn stop_id(&self) -> Option<i32> {
292 self.stop_id
293 }
294
295 pub fn administration_1(&self) -> &str {
296 &self.administration_1
297 }
298
299 pub fn administration_2(&self) -> &str {
300 &self.administration_2
301 }
302
303 pub fn duration(&self) -> i16 {
304 self.duration
305 }
306}
307
308#[derive(Debug, Serialize, Deserialize)]
313pub struct ExchangeTimeJourney {
314 id: i32,
315 stop_id: i32,
316 journey_legacy_id_1: i32,
317 administration_1: String,
318 journey_legacy_id_2: i32,
319 administration_2: String,
320 duration: i16, is_guaranteed: bool,
322 bit_field_id: Option<i32>,
323}
324
325impl_Model!(ExchangeTimeJourney);
326
327impl ExchangeTimeJourney {
328 pub fn new(
329 id: i32,
330 stop_id: i32,
331 (journey_legacy_id_1, administration_1): JourneyId,
332 (journey_legacy_id_2, administration_2): JourneyId,
333 duration: i16,
334 is_guaranteed: bool,
335 bit_field_id: Option<i32>,
336 ) -> Self {
337 Self {
338 id,
339 stop_id,
340 journey_legacy_id_1,
341 administration_1,
342 journey_legacy_id_2,
343 administration_2,
344 duration,
345 is_guaranteed,
346 bit_field_id,
347 }
348 }
349
350 pub fn stop_id(&self) -> i32 {
353 self.stop_id
354 }
355
356 pub fn journey_legacy_id_1(&self) -> i32 {
357 self.journey_legacy_id_1
358 }
359
360 pub fn administration_1(&self) -> &str {
361 &self.administration_1
362 }
363
364 pub fn journey_legacy_id_2(&self) -> i32 {
365 self.journey_legacy_id_2
366 }
367
368 pub fn administration_2(&self) -> &str {
369 &self.administration_2
370 }
371
372 pub fn duration(&self) -> i16 {
373 self.duration
374 }
375
376 pub fn bit_field_id(&self) -> Option<i32> {
377 self.bit_field_id
378 }
379}
380
381#[derive(Debug, Serialize, Deserialize)]
386pub struct ExchangeTimeLine {
387 id: i32,
388 stop_id: Option<i32>,
389 line_1: LineInfo,
390 line_2: LineInfo,
391 duration: i16, is_guaranteed: bool,
393}
394
395impl_Model!(ExchangeTimeLine);
396
397#[derive(Debug, Serialize, Deserialize)]
398pub(crate) struct LineInfo {
399 administration: String,
400 transport_type_id: i32,
401 line_id: Option<String>,
402 direction: Option<DirectionType>,
403}
404
405impl LineInfo {
406 pub(crate) fn new(
407 administration: String,
408 transport_type_id: i32,
409 line_id: Option<String>,
410 direction: Option<DirectionType>,
411 ) -> Self {
412 Self {
413 administration,
414 transport_type_id,
415 line_id,
416 direction,
417 }
418 }
419}
420
421impl ExchangeTimeLine {
422 pub(crate) fn new(
423 id: i32,
424 stop_id: Option<i32>,
425 line_1: LineInfo,
426 line_2: LineInfo,
427 duration: i16,
428 is_guaranteed: bool,
429 ) -> Self {
430 Self {
431 id,
432 stop_id,
433 line_1,
434 line_2,
435 duration,
436 is_guaranteed,
437 }
438 }
439}
440
441#[derive(Debug, Serialize, Deserialize)]
446pub struct InformationText {
447 id: i32,
448 content: FxHashMap<Language, String>,
449}
450
451impl_Model!(InformationText);
452
453impl InformationText {
454 pub fn new(id: i32) -> Self {
455 Self {
456 id,
457 content: FxHashMap::default(),
458 }
459 }
460
461 pub fn set_content(&mut self, language: Language, value: &str) {
464 self.content.insert(language, value.to_string());
465 }
466}
467
468#[derive(Debug, Default, Serialize, Deserialize)]
473pub struct Journey {
474 id: i32,
475 legacy_id: i32,
476 administration: String,
477 metadata: FxHashMap<JourneyMetadataType, Vec<JourneyMetadataEntry>>,
478 route: Vec<JourneyRouteEntry>,
479}
480
481impl_Model!(Journey);
482
483impl Journey {
484 pub fn new(id: i32, legacy_id: i32, administration: String) -> Self {
485 Self {
486 id,
487 legacy_id,
488 administration,
489 metadata: FxHashMap::default(),
490 route: Vec::new(),
491 }
492 }
493
494 pub fn administration(&self) -> &str {
497 &self.administration
498 }
499
500 pub fn legacy_id(&self) -> i32 {
501 self.legacy_id
502 }
503
504 fn metadata(&self) -> &FxHashMap<JourneyMetadataType, Vec<JourneyMetadataEntry>> {
505 &self.metadata
506 }
507
508 pub fn route(&self) -> &Vec<JourneyRouteEntry> {
509 &self.route
510 }
511
512 pub fn add_metadata_entry(&mut self, k: JourneyMetadataType, v: JourneyMetadataEntry) {
515 self.metadata.entry(k).or_default().push(v);
516 }
517
518 pub fn add_route_entry(&mut self, entry: JourneyRouteEntry) {
519 self.route.push(entry);
520 }
521
522 pub(crate) fn bit_field_id(&self) -> JResult<Option<i32>> {
523 let entry = self
524 .metadata()
525 .get(&JourneyMetadataType::BitField)
526 .ok_or(JourneyError::MissingBitFieldMetadata)?;
527
528 Ok(entry
529 .first()
530 .ok_or(JourneyError::EmptyJourneyMetadata)?
531 .bit_field_id)
532 }
533
534 pub fn transport_type_id(&self) -> HResult<i32> {
535 let entry = self
536 .metadata()
537 .get(&JourneyMetadataType::TransportType)
538 .ok_or(JourneyError::MissingTransportType)?;
539 entry
540 .first()
541 .ok_or::<HrdfError>((JourneyError::EmptyJourneyMetadata).into())?
542 .resource_id
543 .ok_or(JourneyError::MissingRessourceId.into())
544 }
545
546 pub fn transport_type<'a>(
547 &'a self,
548 data_storage: &'a DataStorage,
549 ) -> HResult<&'a TransportType> {
550 let transport_id = self.transport_type_id()?;
551 data_storage
552 .transport_types()
553 .find(transport_id)
554 .ok_or(JourneyError::TransportIdNotFound(transport_id).into())
555 }
556
557 pub fn first_stop_id(&self) -> HResult<i32> {
558 Ok(self
559 .route
560 .first()
561 .ok_or(JourneyError::EmptyRoute)?
562 .stop_id())
563 }
564
565 pub fn is_first_stop(&self, stop_id: i32, ignore_loop: bool) -> HResult<bool> {
566 if ignore_loop && self.first_stop_id()? == self.last_stop_id()? {
567 Ok(false)
568 } else {
569 Ok(stop_id == self.first_stop_id()?)
570 }
571 }
572
573 pub fn last_stop_id(&self) -> HResult<i32> {
574 Ok(self.route.last().ok_or(JourneyError::EmptyRoute)?.stop_id())
575 }
576
577 pub fn is_last_stop(&self, stop_id: i32, ignore_loop: bool) -> HResult<bool> {
578 if ignore_loop && self.first_stop_id()? == self.last_stop_id()? {
579 Ok(false)
580 } else {
581 Ok(stop_id == self.last_stop_id()?)
582 }
583 }
584
585 pub fn count_stops(&self, departure_stop_id: i32, arrival_stop_id: i32) -> usize {
586 self.route()
587 .iter()
588 .skip_while(|stop| stop.stop_id() != departure_stop_id)
589 .take_while(|stop| stop.stop_id() != arrival_stop_id)
590 .count()
591 + 1
592 }
593
594 pub fn hash_route(&self, departure_stop_id: i32) -> Option<u64> {
595 let index = self
596 .route
597 .iter()
598 .position(|route_entry| route_entry.stop_id() == departure_stop_id)?;
599
600 let mut hasher = DefaultHasher::new();
601 self.route
602 .iter()
603 .skip(index)
604 .map(|route_entry| route_entry.stop_id())
605 .collect::<BTreeSet<_>>()
606 .hash(&mut hasher);
607 Some(hasher.finish())
608 }
609
610 pub fn departure_time_of(&self, stop_id: i32) -> HResult<(NaiveTime, bool)> {
613 let route = self.route();
614 let index = route
615 .iter()
616 .position(|route_entry| route_entry.stop_id() == stop_id)
617 .ok_or_else(|| HrdfError::MissingStopId(stop_id))?;
618 let departure_time = route[index]
619 .departure_time()
620 .ok_or_else(|| HrdfError::MissingDepartureTime(index))?;
621
622 Ok((
623 departure_time,
624 departure_time
626 < route
627 .first()
628 .ok_or(HrdfError::MissingRoute)?
629 .departure_time()
630 .ok_or(HrdfError::MissingDepartureTime(0))?,
631 ))
632 }
633
634 pub fn departure_at_of(&self, stop_id: i32, date: NaiveDate) -> HResult<NaiveDateTime> {
638 match self.departure_time_of(stop_id)? {
639 (departure_time, false) => Ok(NaiveDateTime::new(date, departure_time)),
640 (departure_time, true) => Ok(NaiveDateTime::new(add_1_day(date)?, departure_time)),
641 }
642 }
643
644 pub fn departure_at_of_with_origin(
647 &self,
648 stop_id: i32,
649 date: NaiveDate,
650 is_departure_date: bool,
652 origin_stop_id: i32,
653 ) -> HResult<NaiveDateTime> {
654 let (departure_time, is_next_day) = self.departure_time_of(stop_id)?;
655 let (_, origin_is_next_day) = if is_departure_date {
656 self.departure_time_of(origin_stop_id)?
657 } else {
658 self.arrival_time_of(origin_stop_id)?
659 };
660
661 match (is_next_day, origin_is_next_day) {
662 (true, false) => Ok(NaiveDateTime::new(add_1_day(date)?, departure_time)),
663 (false, true) => Ok(NaiveDateTime::new(sub_1_day(date)?, departure_time)),
664 _ => Ok(NaiveDateTime::new(date, departure_time)),
665 }
666 }
667
668 pub fn arrival_at_of(&self, stop_id: i32, date: NaiveDate) -> HResult<NaiveDateTime> {
672 match self.arrival_time_of(stop_id)? {
673 (arrival_time, false) => Ok(NaiveDateTime::new(date, arrival_time)),
674 (arrival_time, true) => Ok(NaiveDateTime::new(add_1_day(date)?, arrival_time)),
675 }
676 }
677
678 pub fn arrival_time_of(&self, stop_id: i32) -> HResult<(NaiveTime, bool)> {
679 let route = self.route();
680 let index = route
681 .iter()
682 .skip(1)
684 .position(|route_entry| route_entry.stop_id() == stop_id)
685 .map(|i| i + 1)
686 .ok_or_else(|| HrdfError::MissingStopId(stop_id))?;
687 let arrival_time = route[index]
688 .arrival_time()
689 .ok_or_else(|| HrdfError::MissingArrivalTime(index))?;
690
691 Ok((
692 arrival_time,
693 arrival_time
695 < route
696 .first()
697 .ok_or(HrdfError::MissingRoute)?
698 .departure_time()
699 .ok_or(HrdfError::MissingDepartureTime(0))?,
700 ))
701 }
702
703 pub fn arrival_at_of_with_origin(
705 &self,
706 stop_id: i32,
707 date: NaiveDate,
708 is_departure_date: bool,
710 origin_stop_id: i32,
711 ) -> HResult<NaiveDateTime> {
712 let (arrival_time, is_next_day) = self.arrival_time_of(stop_id)?;
713 let (_, origin_is_next_day) = if is_departure_date {
714 self.departure_time_of(origin_stop_id)?
715 } else {
716 self.arrival_time_of(origin_stop_id)?
717 };
718
719 match (is_next_day, origin_is_next_day) {
720 (true, false) => Ok(NaiveDateTime::new(add_1_day(date)?, arrival_time)),
721 (false, true) => Ok(NaiveDateTime::new(sub_1_day(date)?, arrival_time)),
722 _ => Ok(NaiveDateTime::new(date, arrival_time)),
723 }
724 }
725
726 pub fn route_section(
728 &self,
729 departure_stop_id: i32,
730 arrival_stop_id: i32,
731 ) -> Vec<&JourneyRouteEntry> {
732 let mut route_iter = self.route().iter();
733
734 for route_entry in route_iter.by_ref() {
735 if route_entry.stop_id() == departure_stop_id {
736 break;
737 }
738 }
739
740 let mut result = Vec::new();
741
742 for route_entry in route_iter {
743 result.push(route_entry);
744
745 if route_entry.stop_id() == arrival_stop_id {
746 break;
747 }
748 }
749
750 result
751 }
752}
753
754type JResult<T> = Result<T, JourneyError>;
755
756#[derive(Debug, Error)]
757pub enum JourneyError {
758 #[error("Missing MitField Metadata")]
759 MissingBitFieldMetadata,
760 #[error("JourneyMetaData is empty")]
761 EmptyJourneyMetadata,
762 #[error("Missing Transport Type Metadata")]
763 MissingTransportType,
764 #[error("Missing Reoussirce Id")]
765 MissingRessourceId,
766 #[error("Transport Id: {0} not found")]
767 TransportIdNotFound(i32),
768 #[error("Empty Route")]
769 EmptyRoute,
770 #[error("Stop Id: {0} not found")]
771 StopIdNotFound(i32),
772}
773
774#[derive(Clone, Copy, Debug, Default, Display, Eq, Hash, PartialEq, Serialize, Deserialize)]
779pub enum JourneyMetadataType {
780 #[default]
781 Attribute,
782 BitField,
783 Direction,
784 InformationText,
785 Line,
786 ExchangeTimeBoarding,
787 ExchangeTimeDisembarking,
788 TransportType,
789}
790
791#[derive(Debug, Serialize, Deserialize)]
796pub struct JourneyMetadataEntry {
797 from_stop_id: Option<i32>,
798 until_stop_id: Option<i32>,
799 resource_id: Option<i32>,
800 bit_field_id: Option<i32>,
801 departure_time: Option<NaiveTime>,
802 arrival_time: Option<NaiveTime>,
803 extra_field_1: Option<String>,
804 extra_field_2: Option<i32>,
805}
806
807impl JourneyMetadataEntry {
808 #[allow(clippy::too_many_arguments)]
809 pub fn new(
810 from_stop_id: Option<i32>,
811 until_stop_id: Option<i32>,
812 resource_id: Option<i32>,
813 bit_field_id: Option<i32>,
814 departure_time: Option<NaiveTime>,
815 arrival_time: Option<NaiveTime>,
816 extra_field_1: Option<String>,
817 extra_field_2: Option<i32>,
818 ) -> Self {
819 Self {
820 from_stop_id,
821 until_stop_id,
822 resource_id,
823 bit_field_id,
824 departure_time,
825 arrival_time,
826 extra_field_1,
827 extra_field_2,
828 }
829 }
830}
831
832#[derive(Debug, Serialize, Deserialize)]
837pub struct JourneyRouteEntry {
838 stop_id: i32,
839 arrival_time: Option<NaiveTime>,
840 departure_time: Option<NaiveTime>,
841}
842
843impl JourneyRouteEntry {
844 pub fn new(
845 stop_id: i32,
846 arrival_time: Option<NaiveTime>,
847 departure_time: Option<NaiveTime>,
848 ) -> Self {
849 Self {
850 stop_id,
851 arrival_time,
852 departure_time,
853 }
854 }
855
856 pub fn stop_id(&self) -> i32 {
859 self.stop_id
860 }
861
862 pub fn arrival_time(&self) -> &Option<NaiveTime> {
863 &self.arrival_time
864 }
865
866 pub fn departure_time(&self) -> &Option<NaiveTime> {
867 &self.departure_time
868 }
869
870 pub fn stop<'a>(&'a self, data_storage: &'a DataStorage) -> HResult<&'a Stop> {
873 let stop_id = self.stop_id();
874 data_storage
875 .stops()
876 .find(stop_id)
877 .ok_or(JourneyError::StopIdNotFound(stop_id).into())
878 }
879}
880
881#[derive(Debug, Serialize, Deserialize)]
886pub struct JourneyPlatform {
887 journey_legacy_id: i32,
888 administration: String,
889 platform_id: i32,
890 time: Option<NaiveTime>,
891 bit_field_id: Option<i32>,
892}
893
894impl JourneyPlatform {
895 pub fn new(
896 journey_legacy_id: i32,
897 administration: String,
898 platform_id: i32,
899 time: Option<NaiveTime>,
900 bit_field_id: Option<i32>,
901 ) -> Self {
902 Self {
903 journey_legacy_id,
904 administration,
905 platform_id,
906 time,
907 bit_field_id,
908 }
909 }
910}
911
912impl Model<JourneyPlatform> for JourneyPlatform {
913 type K = (i32, i32);
914
915 fn id(&self) -> Self::K {
916 (self.journey_legacy_id, self.platform_id)
917 }
918}
919
920#[derive(
925 Clone, Copy, Debug, Default, Display, Eq, Hash, PartialEq, EnumString, Serialize, Deserialize,
926)]
927pub enum Language {
928 #[default]
929 #[strum(serialize = "deu", serialize = "DE")]
930 German,
931
932 #[strum(serialize = "fra", serialize = "FR")]
933 French,
934
935 #[strum(serialize = "ita", serialize = "IT")]
936 Italian,
937
938 #[strum(serialize = "eng", serialize = "EN")]
939 English,
940}
941
942#[derive(Debug, Default, Serialize, Deserialize)]
947pub struct Line {
948 id: i32,
949 name: String,
950 short_name: String,
951 long_name: String,
952 region_name: String,
953 internal_designation: String,
954 description: String,
955 text_color: Color,
956 background_color: Color,
957 infotext_id: i32,
958}
959
960impl_Model!(Line);
961
962impl Line {
963 pub fn new(id: i32, name: String) -> Self {
964 Self {
965 id,
966 name,
967 short_name: String::default(),
968 long_name: String::default(),
969 region_name: String::default(),
970 internal_designation: String::default(),
971 description: String::default(),
972 text_color: Color::default(),
973 background_color: Color::default(),
974 infotext_id: -1,
975 }
976 }
977
978 pub fn set_short_name(&mut self, value: String) {
981 self.short_name = value;
982 }
983
984 pub fn set_long_name(&mut self, value: String) {
985 self.long_name = value;
986 }
987
988 pub fn set_region_name(&mut self, value: String) {
989 self.region_name = value;
990 }
991
992 pub fn set_internal_designation(&mut self, value: String) {
993 self.internal_designation = value;
994 }
995
996 pub fn set_description(&mut self, value: String) {
997 self.description = value;
998 }
999
1000 pub fn set_text_color(&mut self, value: Color) {
1001 self.text_color = value;
1002 }
1003
1004 pub fn set_background_color(&mut self, value: Color) {
1005 self.background_color = value;
1006 }
1007
1008 pub fn set_infotext_id(&mut self, value: i32) {
1009 self.infotext_id = value;
1010 }
1011}
1012
1013#[derive(Debug, Serialize, Deserialize)]
1018pub struct Platform {
1019 id: i32,
1020 name: String,
1021 sectors: Option<String>,
1022 stop_id: i32,
1023 sloid: String,
1024 lv95_coordinates: Coordinates,
1025 wgs84_coordinates: Coordinates,
1026}
1027
1028impl_Model!(Platform);
1029
1030impl Platform {
1031 pub fn new(id: i32, name: String, sectors: Option<String>, stop_id: i32) -> Self {
1032 Self {
1033 id,
1034 name,
1035 sectors,
1036 stop_id,
1037 sloid: String::default(),
1038 lv95_coordinates: Coordinates::default(),
1039 wgs84_coordinates: Coordinates::default(),
1040 }
1041 }
1042
1043 pub fn set_sloid(&mut self, value: String) {
1046 self.sloid = value;
1047 }
1048
1049 pub fn set_lv95_coordinates(&mut self, value: Coordinates) {
1050 self.lv95_coordinates = value;
1051 }
1052
1053 pub fn set_wgs84_coordinates(&mut self, value: Coordinates) {
1054 self.wgs84_coordinates = value;
1055 }
1056}
1057
1058#[derive(Debug, Serialize, Deserialize)]
1063pub struct Stop {
1064 id: i32,
1065 name: String,
1066 long_name: Option<String>,
1067 abbreviation: Option<String>,
1068 synonyms: Option<Vec<String>>,
1069 lv95_coordinates: Option<Coordinates>,
1070 wgs84_coordinates: Option<Coordinates>,
1071 exchange_priority: i16,
1072 exchange_flag: i16,
1073 exchange_time: Option<(i16, i16)>, restrictions: i16,
1075 sloid: String,
1076 boarding_areas: Vec<String>,
1077}
1078
1079impl_Model!(Stop);
1080
1081impl Stop {
1082 pub fn new(
1083 id: i32,
1084 name: String,
1085 long_name: Option<String>,
1086 abbreviation: Option<String>,
1087 synonyms: Option<Vec<String>>,
1088 ) -> Self {
1089 Self {
1090 id,
1091 name,
1092 long_name,
1093 abbreviation,
1094 synonyms,
1095 lv95_coordinates: None,
1096 wgs84_coordinates: None,
1097 exchange_priority: 8, exchange_flag: 0,
1099 exchange_time: None,
1100 restrictions: 0,
1101 sloid: String::default(),
1102 boarding_areas: Vec::new(),
1103 }
1104 }
1105
1106 pub fn name(&self) -> &str {
1109 &self.name
1110 }
1111
1112 pub fn lv95_coordinates(&self) -> Option<Coordinates> {
1113 self.lv95_coordinates
1114 }
1115
1116 pub fn set_lv95_coordinates(&mut self, value: Coordinates) {
1117 self.lv95_coordinates = Some(value);
1118 }
1119
1120 pub fn wgs84_coordinates(&self) -> Option<Coordinates> {
1121 self.wgs84_coordinates
1122 }
1123
1124 pub fn set_wgs84_coordinates(&mut self, value: Coordinates) {
1125 self.wgs84_coordinates = Some(value);
1126 }
1127
1128 pub fn set_exchange_priority(&mut self, value: i16) {
1129 self.exchange_priority = value;
1130 }
1131
1132 pub fn exchange_flag(&self) -> i16 {
1133 self.exchange_flag
1134 }
1135
1136 pub fn set_exchange_flag(&mut self, value: i16) {
1137 self.exchange_flag = value;
1138 }
1139
1140 pub fn exchange_time(&self) -> Option<(i16, i16)> {
1141 self.exchange_time
1142 }
1143
1144 pub fn set_exchange_time(&mut self, value: Option<(i16, i16)>) {
1145 self.exchange_time = value;
1146 }
1147
1148 pub fn set_restrictions(&mut self, value: i16) {
1149 self.restrictions = value;
1150 }
1151
1152 pub fn set_sloid(&mut self, value: String) {
1153 self.sloid = value;
1154 }
1155
1156 pub fn add_boarding_area(&mut self, value: String) {
1159 self.boarding_areas.push(value);
1160 }
1161
1162 pub fn can_be_used_as_exchange_point(&self) -> bool {
1163 self.exchange_flag() != 0
1164 }
1165}
1166
1167#[derive(Debug, Default, Serialize, Deserialize)]
1172pub struct StopConnection {
1173 id: i32,
1174 stop_id_1: i32,
1175 stop_id_2: i32,
1176 duration: i16, attribute: i32,
1178}
1179
1180impl_Model!(StopConnection);
1181
1182impl StopConnection {
1183 pub fn new(id: i32, stop_id_1: i32, stop_id_2: i32, duration: i16) -> Self {
1184 Self {
1185 id,
1186 stop_id_1,
1187 stop_id_2,
1188 duration,
1189 attribute: 0,
1190 }
1191 }
1192
1193 pub fn stop_id_1(&self) -> i32 {
1196 self.stop_id_1
1197 }
1198
1199 pub fn stop_id_2(&self) -> i32 {
1200 self.stop_id_2
1201 }
1202
1203 pub fn duration(&self) -> i16 {
1204 self.duration
1205 }
1206
1207 pub fn set_attribute(&mut self, value: i32) {
1208 self.attribute = value;
1209 }
1210}
1211
1212#[derive(Debug, Serialize, Deserialize)]
1217pub struct ThroughService {
1218 id: i32,
1219 journey_1_id: JourneyId,
1220 journey_1_stop_id: i32, journey_2_id: JourneyId,
1222 journey_2_stop_id: i32, bit_field_id: i32,
1224}
1225
1226impl_Model!(ThroughService);
1227
1228impl ThroughService {
1229 pub fn new(
1230 id: i32,
1231 journey_1_id: JourneyId,
1232 journey_1_stop_id: i32,
1233 journey_2_id: JourneyId,
1234 journey_2_stop_id: i32,
1235 bit_field_id: i32,
1236 ) -> Self {
1237 Self {
1238 id,
1239 journey_1_id,
1240 journey_1_stop_id,
1241 journey_2_id,
1242 journey_2_stop_id,
1243 bit_field_id,
1244 }
1245 }
1246
1247 pub fn journey_1_id(&self) -> &JourneyId {
1248 &self.journey_1_id
1249 }
1250
1251 pub fn journey_1_stop_id(&self) -> i32 {
1252 self.journey_1_stop_id
1253 }
1254
1255 pub fn journey_2_id(&self) -> &JourneyId {
1256 &self.journey_2_id
1257 }
1258
1259 pub fn journey_2_stop_id(&self) -> i32 {
1260 self.journey_2_stop_id
1261 }
1262
1263 pub fn bit_field_id(&self) -> i32 {
1264 self.bit_field_id
1265 }
1266}
1267
1268#[derive(Debug, Serialize, Deserialize)]
1273pub struct TimetableMetadataEntry {
1274 id: i32,
1275 key: String,
1276 value: String,
1277}
1278
1279impl_Model!(TimetableMetadataEntry);
1280
1281impl TimetableMetadataEntry {
1282 pub fn new(id: i32, key: String, value: String) -> Self {
1283 Self { id, key, value }
1284 }
1285
1286 pub fn key(&self) -> &str {
1289 &self.key
1290 }
1291
1292 pub fn value(&self) -> &str {
1293 &self.value
1294 }
1295
1296 pub fn value_as_naive_date(&self) -> NaiveDate {
1298 NaiveDate::parse_from_str(self.value(), "%Y-%m-%d").unwrap()
1299 }
1300}
1301
1302#[derive(Debug, Serialize, Deserialize)]
1307pub struct TransportCompany {
1308 id: i32,
1309 short_name: FxHashMap<Language, String>,
1310 long_name: FxHashMap<Language, String>,
1311 full_name: FxHashMap<Language, String>,
1312 administrations: Vec<String>,
1313}
1314
1315impl_Model!(TransportCompany);
1316
1317impl TransportCompany {
1318 pub fn new(id: i32) -> Self {
1319 Self {
1320 id,
1321 short_name: FxHashMap::default(),
1322 long_name: FxHashMap::default(),
1323 full_name: FxHashMap::default(),
1324 administrations: Vec::new(),
1325 }
1326 }
1327
1328 pub fn set_administrations(&mut self, administrations: Vec<String>) {
1331 self.administrations = administrations;
1332 }
1333
1334 pub fn set_short_name(&mut self, language: Language, value: &str) {
1335 self.short_name.insert(language, value.to_string());
1336 }
1337
1338 pub fn set_long_name(&mut self, language: Language, value: &str) {
1339 self.long_name.insert(language, value.to_string());
1340 }
1341
1342 pub fn set_full_name(&mut self, language: Language, value: &str) {
1343 self.full_name.insert(language, value.to_string());
1344 }
1345}
1346
1347#[derive(Debug, Default, Serialize, Deserialize)]
1352pub struct TransportType {
1353 id: i32,
1354 designation: String,
1355 product_class_id: i16,
1356 tariff_group: String,
1357 output_control: i16,
1358 short_name: String,
1359 surcharge: i16,
1360 flag: String,
1361 product_class_name: FxHashMap<Language, String>,
1362 category_name: FxHashMap<Language, String>,
1363}
1364
1365impl_Model!(TransportType);
1366
1367impl TransportType {
1368 #[allow(clippy::too_many_arguments)]
1369 pub fn new(
1370 id: i32,
1371 designation: String,
1372 product_class_id: i16,
1373 tariff_group: String,
1374 output_control: i16,
1375 short_name: String,
1376 surcharge: i16,
1377 flag: String,
1378 ) -> Self {
1379 Self {
1380 id,
1381 designation,
1382 product_class_id,
1383 tariff_group,
1384 output_control,
1385 short_name,
1386 surcharge,
1387 flag,
1388 product_class_name: FxHashMap::default(),
1389 category_name: FxHashMap::default(),
1390 }
1391 }
1392
1393 pub fn designation(&self) -> &str {
1396 &self.designation
1397 }
1398
1399 pub fn product_class_id(&self) -> i16 {
1400 self.product_class_id
1401 }
1402
1403 pub fn set_product_class_name(&mut self, language: Language, value: &str) {
1404 self.product_class_name.insert(language, value.to_string());
1405 }
1406
1407 pub fn set_category_name(&mut self, language: Language, value: &str) {
1408 self.category_name.insert(language, value.to_string());
1409 }
1410}
1411
1412struct NaiveDateRange(NaiveDate, NaiveDate);
1417
1418impl NaiveDateRange {
1419 fn new(date_from: NaiveDate, date_until: NaiveDate) -> Self {
1420 NaiveDateRange(date_from, date_until)
1421 }
1422 fn contains(&self, date: &NaiveDate) -> bool {
1423 self.0 <= *date && self.1 >= *date
1424 }
1425}
1426
1427#[derive(Clone, Copy, Debug, Display, Eq, Hash, PartialEq, Serialize, Deserialize)]
1428#[allow(non_camel_case_types)]
1429pub enum Version {
1430 V_5_20_1_0,
1431 V_5_40_41_2_0_2,
1432 V_5_40_41_2_0_3,
1433 V_5_40_41_2_0_4,
1434 V_5_40_41_2_0_5,
1435 V_5_40_41_2_0_6,
1436 V_5_40_41_2_0_7,
1437}
1438
1439impl Version {
1440 fn timetable_2026() -> NaiveDateRange {
1441 NaiveDateRange::new(
1442 NaiveDate::from_ymd_opt(2025, 12, 14).unwrap(),
1443 NaiveDate::from_ymd_opt(2026, 12, 12).unwrap(),
1444 )
1445 }
1446 fn timetable_2025() -> NaiveDateRange {
1447 NaiveDateRange::new(
1448 NaiveDate::from_ymd_opt(2024, 12, 15).unwrap(),
1449 NaiveDate::from_ymd_opt(2025, 12, 13).unwrap(),
1450 )
1451 }
1452 fn timetable_2024() -> NaiveDateRange {
1453 NaiveDateRange::new(
1454 NaiveDate::from_ymd_opt(2023, 12, 10).unwrap(),
1455 NaiveDate::from_ymd_opt(2024, 12, 14).unwrap(),
1456 )
1457 }
1458 fn timetable_2023() -> NaiveDateRange {
1459 NaiveDateRange::new(
1460 NaiveDate::from_ymd_opt(2022, 12, 11).unwrap(),
1461 NaiveDate::from_ymd_opt(2023, 12, 13).unwrap(),
1462 )
1463 }
1464 fn timetable_2022() -> NaiveDateRange {
1465 NaiveDateRange::new(
1466 NaiveDate::from_ymd_opt(2021, 12, 12).unwrap(),
1467 NaiveDate::from_ymd_opt(2022, 12, 10).unwrap(),
1468 )
1469 }
1470 pub(crate) fn try_url(date: NaiveDate) -> HResult<String> {
1483 if Self::timetable_2026().contains(&date) {
1484 Ok(String::from(
1485 "https://data.opentransportdata.swiss/en/dataset/timetable-54-2026-hrdf/permalink",
1486 ))
1487 } else if Self::timetable_2025().contains(&date) {
1488 Ok(String::from(
1489 "https://archive.opentransportdata.swiss/timetable_hrdf/timetable-2025-hrdf-54/OeV_Sammlung_CH_HRDF_5_40_41_2025_20251205_205244.zip",
1490 ))
1491 } else if Self::timetable_2024().contains(&date) {
1492 Ok(String::from(
1493 "https://archive.opentransportdata.swiss/timetable_hrdf/timetable-2024-hrdf-54/OeV_Sammlung_CH_HRDF_5_40_41_2024_20241213_205621.zip",
1494 ))
1495 } else if Self::timetable_2023().contains(&date) {
1496 Ok(String::from(
1497 "https://archive.opentransportdata.swiss/timetable_hrdf/timetable-2023-hrdf-54/OeV_Sammlung_CH_HRDF_5_40_41_2023_20231206_204217.zip",
1498 ))
1499 } else if Self::timetable_2022().contains(&date) {
1500 Ok(String::from(
1501 "https://archive.opentransportdata.swiss/timetable_hrdf/timetable-2022-hrdf-54/OeV_Sammlung_CH_HRDF_5_40_41_2022_20221207_205110.zip",
1502 ))
1503 } else {
1512 Err(HrdfError::OutOfRangeDate(date))
1513 }
1514 }
1515}
1516
1517impl TryFrom<NaiveDate> for Version {
1518 type Error = HrdfError;
1519
1520 fn try_from(date: NaiveDate) -> Result<Self, Self::Error> {
1522 if Self::timetable_2026().contains(&date)
1523 || Self::timetable_2025().contains(&date)
1524 || Self::timetable_2024().contains(&date)
1525 {
1526 Ok(Version::V_5_40_41_2_0_7)
1527 } else if Self::timetable_2023().contains(&date) || Self::timetable_2022().contains(&date) {
1528 Ok(Version::V_5_40_41_2_0_5)
1529 } else {
1534 Err(HrdfError::OutOfRangeDate(date))
1535 }
1536 }
1537}
1538
1539#[cfg(test)]
1540mod tests {
1541 use super::*;
1542 use chrono::{NaiveDate, NaiveTime};
1543
1544 fn build_route_entry(
1545 stop_id: i32,
1546 arrival: Option<&str>,
1547 departure: Option<&str>,
1548 ) -> JourneyRouteEntry {
1549 let arrival_time = arrival.map(|value| NaiveTime::parse_from_str(value, "%H:%M").unwrap());
1550 let departure_time =
1551 departure.map(|value| NaiveTime::parse_from_str(value, "%H:%M").unwrap());
1552 JourneyRouteEntry::new(stop_id, arrival_time, departure_time)
1553 }
1554
1555 fn build_midnight_journey() -> Journey {
1556 let mut journey = Journey::new(1, 100, "CH".to_string());
1557 journey.add_route_entry(build_route_entry(1, None, Some("23:50")));
1558 journey.add_route_entry(build_route_entry(2, Some("00:10"), Some("00:15")));
1559 journey.add_route_entry(build_route_entry(3, Some("00:30"), None));
1560 journey
1561 }
1562
1563 #[test]
1564 fn coordinates_accessors_match_system() {
1565 let lv95 = Coordinates::new(CoordinateSystem::LV95, 2600000.0, 1200000.0);
1566 assert_eq!(lv95.easting(), Some(2600000.0));
1567 assert_eq!(lv95.northing(), Some(1200000.0));
1568 assert_eq!(lv95.latitude(), None);
1569 assert_eq!(lv95.longitude(), None);
1570
1571 let wgs84 = Coordinates::new(CoordinateSystem::WGS84, 46.948, 7.447);
1572 assert_eq!(wgs84.easting(), None);
1573 assert_eq!(wgs84.northing(), None);
1574 assert_eq!(wgs84.latitude(), Some(46.948));
1575 assert_eq!(wgs84.longitude(), Some(7.447));
1576 }
1577
1578 #[test]
1579 fn stop_exchange_flag_controls_exchange_point() {
1580 let mut stop = Stop::new(1, "Bern".to_string(), None, None, None);
1581 assert!(!stop.can_be_used_as_exchange_point());
1582 stop.set_exchange_flag(1);
1583 assert!(stop.can_be_used_as_exchange_point());
1584 }
1585
1586 #[test]
1587 fn journey_last_stop_logic_handles_loops() {
1588 let mut journey = Journey::new(1, 100, "CH".to_string());
1589 journey.add_route_entry(build_route_entry(1, None, Some("08:00")));
1590 journey.add_route_entry(build_route_entry(2, Some("08:10"), Some("08:15")));
1591 journey.add_route_entry(build_route_entry(1, Some("08:30"), None));
1592
1593 assert!(journey.is_last_stop(1, false).unwrap());
1594 assert!(!journey.is_last_stop(1, true).unwrap());
1595 assert!(!journey.is_last_stop(2, false).unwrap());
1596 }
1597
1598 #[test]
1599 fn journey_counts_and_sections_are_consistent() {
1600 let mut journey = Journey::new(1, 100, "CH".to_string());
1601 journey.add_route_entry(build_route_entry(1, None, Some("08:00")));
1602 journey.add_route_entry(build_route_entry(2, Some("08:10"), Some("08:15")));
1603 journey.add_route_entry(build_route_entry(3, Some("08:30"), Some("08:35")));
1604 journey.add_route_entry(build_route_entry(4, Some("08:50"), None));
1605
1606 assert_eq!(journey.count_stops(1, 3), 3);
1607 let section = journey.route_section(1, 3);
1608 let ids: Vec<i32> = section.iter().map(|entry| entry.stop_id()).collect();
1609 assert_eq!(ids, vec![2, 3]);
1610 }
1611
1612 #[test]
1613 fn journey_time_calculations_cross_midnight() {
1614 let journey = build_midnight_journey();
1615 let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
1616
1617 let (departure_time, is_next_day) = journey.departure_time_of(2).unwrap();
1618 assert_eq!(departure_time, NaiveTime::from_hms_opt(0, 15, 0).unwrap());
1619 assert!(is_next_day);
1620
1621 let (arrival_time, is_next_day) = journey.arrival_time_of(2).unwrap();
1622 assert_eq!(arrival_time, NaiveTime::from_hms_opt(0, 10, 0).unwrap());
1623 assert!(is_next_day);
1624
1625 let departure_at = journey.departure_at_of(2, date).unwrap();
1626 assert_eq!(
1627 departure_at,
1628 NaiveDate::from_ymd_opt(2024, 1, 2)
1629 .unwrap()
1630 .and_time(NaiveTime::from_hms_opt(0, 15, 0).unwrap())
1631 );
1632
1633 let arrival_at = journey.arrival_at_of_with_origin(2, date, true, 1).unwrap();
1634 assert_eq!(
1635 arrival_at,
1636 NaiveDate::from_ymd_opt(2024, 1, 2)
1637 .unwrap()
1638 .and_time(NaiveTime::from_hms_opt(0, 10, 0).unwrap())
1639 );
1640 }
1641
1642 #[test]
1643 fn journey_bit_field_id_requires_metadata() {
1644 let journey = Journey::new(1, 100, "CH".to_string());
1645 let err = journey.bit_field_id().unwrap_err();
1646 match err {
1647 JourneyError::MissingBitFieldMetadata => {}
1648 other => panic!("Error should be MissingBitFieldMetadata but is: {other:?}"),
1649 }
1650 }
1651
1652 #[test]
1653 fn timetable_metadata_entry_parses_date() {
1654 let entry =
1655 TimetableMetadataEntry::new(1, "start_date".to_string(), "2024-12-15".to_string());
1656 assert_eq!(
1657 entry.value_as_naive_date(),
1658 NaiveDate::from_ymd_opt(2024, 12, 15).unwrap()
1659 );
1660 }
1661
1662 #[test]
1663 fn version_resolution_matches_date_ranges() {
1664 let in_2026 = NaiveDate::from_ymd_opt(2026, 6, 1).unwrap();
1665 assert_eq!(
1666 Version::try_from(in_2026).unwrap(),
1667 Version::V_5_40_41_2_0_7
1668 );
1669 let url = Version::try_url(in_2026).unwrap();
1670 assert!(url.contains(
1671 "https://data.opentransportdata.swiss/en/dataset/timetable-54-2026-hrdf/permalink"
1672 ));
1673
1674 let in_2025 = NaiveDate::from_ymd_opt(2025, 6, 1).unwrap();
1675 assert_eq!(
1676 Version::try_from(in_2025).unwrap(),
1677 Version::V_5_40_41_2_0_7
1678 );
1679 let url = Version::try_url(in_2025).unwrap();
1680 assert!(url.contains("timetable-2025-hrdf"));
1681
1682 let in_2024 = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
1683 assert_eq!(
1684 Version::try_from(in_2024).unwrap(),
1685 Version::V_5_40_41_2_0_7
1686 );
1687 let url = Version::try_url(in_2024).unwrap();
1688 assert!(url.contains("timetable-2024-hrdf"));
1689
1690 let in_2023 = NaiveDate::from_ymd_opt(2023, 6, 1).unwrap();
1691 assert_eq!(
1692 Version::try_from(in_2023).unwrap(),
1693 Version::V_5_40_41_2_0_5
1694 );
1695 let url = Version::try_url(in_2023).unwrap();
1696 assert!(url.contains("timetable-2023-hrdf"));
1697
1698 let in_2022 = NaiveDate::from_ymd_opt(2022, 6, 1).unwrap();
1699 assert_eq!(
1700 Version::try_from(in_2022).unwrap(),
1701 Version::V_5_40_41_2_0_5
1702 );
1703 let url = Version::try_url(in_2022).unwrap();
1704 assert!(url.contains("timetable-2022-hrdf"));
1705 }
1706
1707 #[test]
1708 #[should_panic]
1709 fn version_resolution_not_matching_date_ranges() {
1710 let in_2021 = NaiveDate::from_ymd_opt(2021, 6, 1).unwrap();
1711 Version::try_from(in_2021).unwrap();
1712 }
1713
1714 #[test]
1715 #[should_panic]
1716 fn url_resolution_not_matching_date_ranges() {
1717 let in_2021 = NaiveDate::from_ymd_opt(2021, 6, 1).unwrap();
1718 Version::try_url(in_2021).unwrap();
1719 }
1720}