collisions/
lib.rs

1//! A simple data format to list collisions that've occurred in the real world. The data is
2//! serializable in a binary format or as JSON.
3
4#[macro_use]
5extern crate log;
6
7use geom::{Duration, LonLat};
8use kml::ExtraShapes;
9use serde::{Deserialize, Serialize};
10
11/// A single dataset describing some collisions that happened.
12#[derive(Serialize, Deserialize)]
13pub struct CollisionDataset {
14    /// A URL pointing to the original data source.
15    pub source_url: String,
16    /// The collisions imported from the data source.
17    pub collisions: Vec<Collision>,
18}
19
20/// A single collision that occurred in the real world.
21#[derive(Serialize, Deserialize)]
22pub struct Collision {
23    /// A single point describing where the collision occurred.
24    pub location: LonLat,
25    /// The local time the collision occurred.
26    // TODO Wait, why isn't this Time?
27    pub time: Duration,
28    /// The severity reported in the original data source.
29    pub severity: Severity,
30    /* TODO Many more interesting and common things: the date, the number of
31     * people/vehicles/bikes/casualties, road/weather/alcohol/speeding conditions possibly
32     * influencing the event, etc. */
33}
34
35/// A simple ranking for how severe the collision was. Different agencies use different
36/// classification systems, each of which likely has their own nuance and bias. This is
37/// deliberately simplified.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
39pub enum Severity {
40    Slight,
41    Serious,
42    Fatal,
43}
44
45/// Import data from the UK STATS19 dataset. See https://github.com/ropensci/stats19. Any parsing
46/// errors will skip the row and log a warning.
47pub fn import_stats19(input: ExtraShapes, source_url: &str) -> CollisionDataset {
48    let mut data = CollisionDataset {
49        source_url: source_url.to_string(),
50        collisions: Vec::new(),
51    };
52    for shape in input.shapes {
53        if shape.points.len() != 1 {
54            warn!("One row had >1 point: {:?}", shape);
55            continue;
56        }
57        let time = match Duration::parse(&format!("{}:00", shape.attributes["Time"])) {
58            Ok(time) => time,
59            Err(err) => {
60                warn!("Couldn't parse time: {}", err);
61                continue;
62            }
63        };
64        let severity = match shape.attributes["Accident_Severity"].as_ref() {
65            // TODO Is this backwards?
66            "1" => Severity::Slight,
67            "2" => Severity::Serious,
68            "3" => Severity::Fatal,
69            x => {
70                warn!("Unknown severity {}", x);
71                continue;
72            }
73        };
74        data.collisions.push(Collision {
75            location: shape.points[0],
76            time,
77            severity,
78        });
79    }
80    data
81}
82
83/// Import data from Seattle GeoData
84/// (https://data-seattlecitygis.opendata.arcgis.com/datasets/5b5c745e0f1f48e7a53acec63a0022ab_0).
85/// Any parsing errors will skip the row and log a warning.
86pub fn import_seattle(input: ExtraShapes, source_url: &str) -> CollisionDataset {
87    let mut data = CollisionDataset {
88        source_url: source_url.to_string(),
89        collisions: Vec::new(),
90    };
91    for shape in input.shapes {
92        if shape.points.len() != 1 {
93            warn!("One row had >1 point: {:?}", shape);
94            continue;
95        }
96        let time = match parse_incdttm(&shape.attributes["INCDTTM"]) {
97            Some(time) => time,
98            None => {
99                warn!("Couldn't parse time {}", shape.attributes["INCDTTM"]);
100                continue;
101            }
102        };
103        let severity = match shape
104            .attributes
105            .get("SEVERITYCODE")
106            .cloned()
107            .unwrap_or_else(String::new)
108            .as_ref()
109        {
110            "1" | "0" => Severity::Slight,
111            "2b" | "2" => Severity::Serious,
112            "3" => Severity::Fatal,
113            x => {
114                warn!("Unknown severity {}", x);
115                continue;
116            }
117        };
118        data.collisions.push(Collision {
119            location: shape.points[0],
120            time,
121            severity,
122        });
123    }
124    data
125}
126
127// INCDTTM is something like "11/12/2019 7:30:00 AM"
128fn parse_incdttm(x: &str) -> Option<Duration> {
129    let parts = x.split(' ').collect::<Vec<_>>();
130    if parts.len() != 3 {
131        return None;
132    }
133    let time = Duration::parse(parts[1]).ok()?;
134    if parts[2] == "AM" {
135        Some(time)
136    } else if parts[2] == "PM" {
137        Some(time + Duration::hours(12))
138    } else {
139        None
140    }
141}