gistools/readers/gtfs/schedule/
shapes.rs

1use crate::readers::csv::parse_csv_as_record;
2use alloc::{collections::BTreeMap, string::String, vec::Vec};
3use s2json::{
4    BBox3D, MValue, MValueCompatible, VectorFeature, VectorGeometry, VectorLineString, VectorPoint,
5};
6
7/// # Shape Properties
8#[derive(Debug, Default, Clone, PartialEq, MValueCompatible)]
9pub struct GTFSShapeProperties {
10    /// The ID of the shape
11    pub shape_id: String,
12}
13impl From<String> for GTFSShapeProperties {
14    fn from(shape_id: String) -> Self {
15        GTFSShapeProperties { shape_id }
16    }
17}
18
19/// # Shape MValue
20#[derive(Debug, Default, Clone, PartialEq, MValueCompatible)]
21pub struct GTFSShapeMValue {
22    /// May be missing
23    pub shape_dist_traveled: f64,
24}
25impl From<f64> for GTFSShapeMValue {
26    fn from(shape_dist_traveled: f64) -> Self {
27        GTFSShapeMValue { shape_dist_traveled }
28    }
29}
30
31/// # Shape Feature
32impl From<GTFSShapes> for VectorFeature {
33    fn from(shapes: GTFSShapes) -> Self {
34        let bbox = BBox3D::from_linestring(&shapes.shapes);
35        let props: GTFSShapeProperties = shapes.shape_id.into();
36        VectorFeature {
37            properties: props.into(),
38            geometry: VectorGeometry::new_linestring(shapes.shapes, Some(bbox)),
39            ..Default::default()
40        }
41    }
42}
43
44/// # Shapes
45///
46/// A collection of shapes all with the same shape_id and ordered by shape_pt_sequence
47#[derive(Debug, Default, Clone, PartialEq)]
48pub struct GTFSShapes {
49    /// The ID of the shape
50    pub shape_id: String,
51    /// The collection of shapes
52    pub shapes: VectorLineString,
53}
54impl From<&Vec<GTFSShape>> for GTFSShapes {
55    fn from(shapes: &Vec<GTFSShape>) -> Self {
56        let mut res = GTFSShapes::default();
57        if shapes.is_empty() {
58            return res;
59        }
60        // set id
61        let id = shapes[0].shape_id.clone();
62        res.shape_id = id;
63        // store shapes
64        for shape in shapes {
65            let mvalue: Option<GTFSShapeMValue> = shape.shape_dist_traveled.map(Into::into);
66            let mvalue: Option<MValue> = mvalue.map(Into::into);
67            res.shapes.push(VectorPoint::new_xy(shape.shape_pt_lon, shape.shape_pt_lat, mvalue));
68        }
69        res
70    }
71}
72
73/// # Shapes
74///
75/// ## Details
76/// **Optional** - Primary key (shape_id, shape_pt_sequence)
77///
78/// Shapes describe the path that a vehicle travels along a route alignment, and are defined in the
79/// file shapes.txt. Shapes are associated with Trips, and consist of a sequence of points through
80/// which the vehicle passes in order. Shapes do not need to intercept the location of Stops
81/// exactly, but all Stops on a trip should lie within a small distance of the shape for that trip,
82/// i.e. close to straight line segments connecting the shape points. The shapes.txt file should be
83/// included for all route-based services (not for zone-based demand-responsive services).
84#[derive(Debug, Default, Clone, PartialEq, MValueCompatible)]
85pub struct GTFSShape {
86    /// **Required**
87    /// Identifies a shape. E.g. "A_shp"
88    pub shape_id: String,
89    /// **Required**
90    /// Latitude of a shape point. Each record in shapes.txt represents a shape point used to define
91    /// the shape.
92    pub shape_pt_lat: f64,
93    /// **Required**
94    /// Longitude of a shape point.
95    pub shape_pt_lon: f64,
96    /// **Required**
97    /// Sequence in which the shape points connect to form the shape. Values must increase along the
98    /// trip but do not need to be consecutive.
99    ///
100    /// Example: If the shape "A_shp" has three points in its definition, the shapes.txt file might contain these records to define the shape:
101    /// shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence
102    /// A_shp,37.61956,-122.48161,0
103    /// A_shp,37.64430,-122.41070,6
104    /// A_shp,37.65863,-122.30839,11
105    pub shape_pt_sequence: u64,
106    /// **Optional**
107    /// Actual distance traveled along the shape from the first shape point to the point specified
108    /// in this record. Used by trip planners to show the correct portion of the shape on a map.
109    /// Values must increase along with shape_pt_sequence; they must not be used to show reverse
110    /// travel along a route. Distance units must be consistent with those used in stop_times.txt.
111    ///
112    /// Recommended for routes that have looping or inlining (the vehicle crosses or travels over
113    /// the same portion of alignment in one trip).
114    ///
115    /// If a vehicle retraces or crosses the route alignment at points in the course of a trip,
116    /// shape_dist_traveled is important to clarify how portions of the points in shapes.txt line
117    /// up correspond with records in stop_times.txt.
118    ///
119    /// Example: If a bus travels along the three points defined above for A_shp, the additional shape_dist_traveled values (shown here in kilometers) would look like this:
120    /// shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence,shape_dist_traveled
121    /// A_shp,37.61956,-122.48161,0,0
122    /// A_shp,37.64430,-122.41070,6,6.8310
123    /// A_shp,37.65863,-122.30839,11,15.8765
124    pub shape_dist_traveled: Option<f64>,
125}
126impl GTFSShape {
127    /// Create a new GTFSShape
128    pub fn new(source: &str) -> BTreeMap<String, Vec<GTFSShape>> {
129        let mut res = BTreeMap::new();
130        for record in parse_csv_as_record::<GTFSShape>(source, None, None) {
131            let v = res.entry(record.shape_id.clone()).or_insert(Vec::new());
132            v.push(record);
133        }
134        // iterate through each and sort by shape_pt_sequence
135        for shapes in res.values_mut() {
136            shapes.sort_by(|a, b| a.shape_pt_sequence.cmp(&b.shape_pt_sequence));
137        }
138        res
139    }
140}