gistools/readers/gtfs/schedule/
stops.rs

1use crate::readers::csv::parse_csv_as_record;
2use alloc::{collections::BTreeMap, string::String};
3use s2json::{MValueCompatible, VectorFeature, VectorGeometry, VectorPoint};
4
5/// Properties object from GTFS stops
6#[derive(Debug, Default, Clone, PartialEq, MValueCompatible)]
7pub struct GTFSStopProperties {
8    /// Stop ID
9    pub stop_id: String,
10    /// Stop code
11    pub stop_code: String,
12    /// Stop name
13    pub stop_name: String,
14    /// TTS stop name
15    pub tts_stop_name: String,
16    /// Stop description
17    pub stop_desc: String,
18    /// Stop zone id
19    pub zone_id: String,
20    /// Stop url
21    pub stop_url: String,
22    /// Location type
23    pub location_type: i8,
24    /// Parent station
25    pub parent_station: String,
26    /// Stop timezone
27    pub stop_timezone: String,
28    /// Wheelchair boarding
29    pub wheelchair_boarding: i8,
30    /// Level id
31    pub level_id: String,
32    /// Platform code
33    pub platform_code: String,
34}
35impl From<&GTFSStop> for GTFSStopProperties {
36    fn from(stop: &GTFSStop) -> Self {
37        GTFSStopProperties {
38            stop_id: stop.stop_id.clone(),
39            stop_code: stop.stop_code.clone(),
40            stop_name: stop.stop_name.clone(),
41            tts_stop_name: stop.tts_stop_name.clone(),
42            stop_desc: stop.stop_desc.clone(),
43            zone_id: stop.zone_id.clone(),
44            stop_url: stop.stop_url.clone(),
45            location_type: stop.location_type.unwrap_or(0),
46            parent_station: stop.parent_station.clone(),
47            stop_timezone: stop.stop_timezone.clone(),
48            wheelchair_boarding: stop.wheelchair_boarding.unwrap_or(0),
49            level_id: stop.level_id.clone(),
50            platform_code: stop.platform_code.clone(),
51        }
52    }
53}
54
55/// Location type. Valid options:
56/// - 0 or empty = Stop/Platform,
57/// - 1 = Station,
58/// - 2 = Entrance/Exit,
59/// - 3 = Generic Node,
60/// - 4 = Boarding Area.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
62pub enum GTFSStopLocationType {
63    /// Stop
64    Stop = 0,
65    /// Station
66    Station = 1,
67    /// Entrance
68    Entrance = 2,
69    /// Generic Node
70    GenericNode = 3,
71    /// Boarding Area
72    BoardingArea = 4,
73}
74impl From<i8> for GTFSStopLocationType {
75    fn from(value: i8) -> Self {
76        match value {
77            1 => GTFSStopLocationType::Station,
78            2 => GTFSStopLocationType::Entrance,
79            3 => GTFSStopLocationType::GenericNode,
80            4 => GTFSStopLocationType::BoardingArea,
81            _ => GTFSStopLocationType::Stop,
82        }
83    }
84}
85
86/// # Stop Information
87///
88/// ## Details
89/// **Conditionally Required** - Stops where vehicles pick up or drop off riders.
90/// Also defines stations, entrances, etc.
91#[derive(Debug, Default, Clone, PartialEq, MValueCompatible)]
92pub struct GTFSStop {
93    /// **Required**
94    /// Identifies a location: stop/platform, station, entrance/exit, generic node, or boarding area.
95    /// Must be unique across:
96    /// - stops.stop_id
97    /// - locations.geojson id
98    /// - location_groups.location_group_id
99    ///
100    /// Multiple routes may use the same `stop_id`.
101    pub stop_id: String,
102    /// **Optional**
103    /// Short text or a number that identifies the location for riders.
104    pub stop_code: String,
105    /// **Conditionally Required**
106    /// Name of the location. Required if `location_type` is 0, 1, or 2. Optional otherwise.
107    pub stop_name: String,
108    /// **Optional**
109    /// Readable version of the stop_name for text-to-speech systems.
110    pub tts_stop_name: String,
111    /// **Optional**
112    /// Description providing useful information about the location.
113    /// Should not be a duplicate of `name`.
114    pub stop_desc: String,
115    /// **Conditionally Required**
116    /// Latitude of the location. Required if `location_type` is 0, 1, or 2. Optional otherwise.
117    pub stop_lat: Option<f64>,
118    /// **Conditionally Required**
119    /// Longitude of the location. Required if `location_type` is 0, 1, or 2. Optional otherwise.
120    pub stop_lon: Option<f64>,
121    /// **Optional**
122    /// Identifies the fare zone for a stop.
123    pub zone_id: String,
124    /// **Optional**
125    /// URL of a web page about this location.
126    pub stop_url: String,
127    /// **Optional**
128    /// Location type. Valid options:
129    /// 0 or empty = Stop/Platform,
130    /// 1 = Station,
131    /// 2 = Entrance/Exit,
132    /// 3 = Generic Node,
133    /// 4 = Boarding Area.
134    pub location_type: Option<i8>,
135    /// **Conditionally Required**
136    /// Defines hierarchy between different locations. Required if `location_type` is 2, 3, or 4.
137    pub parent_station: String,
138    /// **Optional**
139    /// Timezone of the location. Inherits from parent station if not specified.
140    pub stop_timezone: String,
141    /// **Optional**
142    /// Indicates whether wheelchair boardings are possible at this location.
143    /// For parentless stops: 0 = no info, 1 = possible, 2 = not possible.
144    /// For child stops, entrance/exits: inherits or overrides parent station accessibility.
145    pub wheelchair_boarding: Option<i8>,
146    /// **Optional**
147    /// Level of the location. References levels.level_id.
148    pub level_id: String,
149    /// **Optional**
150    /// Platform identifier for a platform stop.
151    pub platform_code: String,
152}
153impl GTFSStop {
154    /// Create a new GTFSStop
155    pub fn new(source: &str) -> BTreeMap<String, GTFSStop> {
156        let mut res = BTreeMap::new();
157        for record in parse_csv_as_record::<GTFSStop>(source, None, None) {
158            res.insert(record.stop_id.clone(), record);
159        }
160        res
161    }
162    /// Get the location_type
163    pub fn get_location_type(&self) -> Option<GTFSStopLocationType> {
164        self.location_type.map(GTFSStopLocationType::from)
165    }
166    /// Convert to a feature
167    pub fn to_feature(&self) -> Option<VectorFeature> {
168        if self.stop_lon.is_none() || self.stop_lat.is_none() {
169            return None;
170        }
171        let properties: GTFSStopProperties = self.into();
172        Some(VectorFeature {
173            properties: properties.into(),
174            geometry: VectorGeometry::new_point(
175                VectorPoint::new_xy(self.stop_lon.unwrap(), self.stop_lat.unwrap(), None),
176                None,
177            ),
178            ..Default::default()
179        })
180    }
181}