osm_geo_mapper/
operations.rs

1use std::{
2    collections::{ BTreeMap, HashMap },
3    convert::TryInto,
4    sync::{ Arc, RwLock },
5};
6use log::warn;
7use geo_types as gt;
8use geojson as gj;
9use osmpbfreader::objects::{ OsmId, OsmObj };
10use osm_xml;
11
12use crate::{
13    features::{Address, GeoTileProperties, GeoTilesDataStructure, TILE_SCALE},
14    operations::{
15        line_string_operations::{draw_line_string, line_string_feature_to_geo_tile},
16        point_operations::{draw_point, point_feature_to_geo_tile},
17        polygon_operations::{draw_polygon, polygon_feature_to_geo_tile},
18    },
19    openstreetmap,
20    osmtogeojson,
21    pbf_parser::HasCoordinates,
22};
23
24pub mod line_string_operations;
25pub mod point_operations;
26pub mod polygon_operations;
27
28// Takes a lat/lon unit (f64) and converts it to a 2d grid coordinate unit using i32.
29// This is a lossy operation.
30pub fn to_tile_scale(unit: f64) -> i32 {
31    return (unit * TILE_SCALE).round() as i32
32}
33
34// Takes a tile-scaled i32 unit and converts it back to a lat/lon scale unit (f64).
35// This is not technically a lossy operation, but the initial convertion to tile scale would have been,
36// therefor you can't expect to be able to convert back-and-forth without losing fidelity.
37pub fn from_tile_scale(unit : i32) -> f64 {
38    return (unit as f64) / TILE_SCALE;
39}
40
41// Same as from_tile_scale(i32) except takes a u8.
42pub fn from_tile_scale_u8(unit : u8) -> f64 {
43    return (unit as f64) / TILE_SCALE;
44}
45
46pub fn property_to_option_string(props: &dyn GeoTileProperties, key: &str) -> Option<String> {
47    match props.fetch(key) {
48        Some(value) => Some(value.to_string()),
49        _ => None,
50    }
51}
52
53pub fn address_from_properties(props: &dyn GeoTileProperties) -> Option<Address> {
54    if props.has("addr:housenumber")
55        || props.has("addr:unit")
56        || props.has("addr:street")
57        || props.has("addr:postcode")
58    {
59        let house_number = match props.fetch("addr:housenumber") {
60            Some(value) => Some(String::from(value)),
61            _ => None,
62        };
63        let unit = match props.fetch("addr:unit") {
64            Some(value) => Some(String::from(value)),
65            _ => None,
66        };
67        let street = match props.fetch("addr:street") {
68            Some(value) => Some(String::from(value)),
69            _ => None,
70        };
71        let postal_code = match props.fetch("addr:postcode") {
72            Some(value) => Some(String::from(value)),
73            _ => None,
74        };
75        Some(Address {
76            house_number,
77            unit,
78            street,
79            postal_code,
80        })
81    } else {
82        None
83    }
84}
85
86pub fn get_geojson_file_by_lat_lon(
87    lat: f64,
88    lon: f64,
89    radius: f64,
90) -> Result<String, Box<dyn std::error::Error>> {
91    let left = lon - radius;
92    let bottom = lat - radius;
93    let right = lon + radius;
94    let top = lat + radius;
95    let osm_file = openstreetmap::download_osm_data_by_bbox(left, bottom, right, top)?;
96    let geojson_file = format!("{}.geojson", osm_file);
97    osmtogeojson::convert_osm_to_geojson(osm_file, geojson_file.clone())?;
98    Ok(geojson_file)
99}
100
101pub fn process_geojson(geojson: &gj::GeoJson) -> GeoTilesDataStructure {
102    let data_structure = GeoTilesDataStructure::new(RwLock::new(HashMap::new()));
103    process_geojson_with_data_structure(geojson, data_structure.clone());
104    data_structure
105}
106
107pub fn process_osm(osm_data: &osm_xml::OSM) -> GeoTilesDataStructure {
108    let data_structure = GeoTilesDataStructure::new(RwLock::new(HashMap::new()));
109    process_osm_with_data_structure(osm_data, data_structure.clone());
110    data_structure
111}
112
113pub fn process_pbf(pbf_data: &BTreeMap<OsmId, OsmObj>) -> GeoTilesDataStructure {
114    let data_structure = GeoTilesDataStructure::new(RwLock::new(HashMap::new()));
115    process_pbf_with_data_structure(pbf_data, data_structure.clone());
116    data_structure
117}
118
119pub fn process_geojson_with_data_structure(geojson: &gj::GeoJson, data_structure: GeoTilesDataStructure) {
120    match *geojson {
121        gj::GeoJson::FeatureCollection(ref ctn) => {
122            for feature in &ctn.features {
123                // Only process features that have properties and a geometry.
124                if feature.properties.is_some() && feature.geometry.is_some() {
125                    process_feature(
126                        feature.properties.as_ref().unwrap(),
127                        &feature.geometry.as_ref().unwrap(),
128                        data_structure.clone(),
129                    )
130                } else {
131                    warn!("Found feature from features without properties or geometry");
132                }
133            }
134        }
135        gj::GeoJson::Feature(ref feature) => {
136            // Only process features that have properties and a geometry.
137            if feature.properties.is_some() && feature.geometry.is_some() {
138                process_feature(
139                    feature.properties.as_ref().unwrap(),
140                    &feature.geometry.as_ref().unwrap(),
141                    data_structure,
142                )
143            } else {
144                warn!("Found feature without properties or geometry");
145            }
146        }
147        gj::GeoJson::Geometry(_) => {
148            // For now, ignore hanging geometry types.
149            //match_geometry(geometry, terrain_type, terrain_manager)
150            warn!("Found top-level geometry")
151        }
152    }
153}
154
155pub fn process_osm_with_data_structure(osm_data: &osm_xml::OSM, data_structure: GeoTilesDataStructure) {
156    // Nodes
157    for (_, node) in osm_data.nodes.iter() {
158        let point: gt::Point<f64> = (node.lat, node.lon).try_into().unwrap();
159        let geo_tile = Arc::new(point_feature_to_geo_tile(&node.tags, point));
160        draw_point(&point, geo_tile, data_structure.clone());
161    }
162    // Ways
163    for (_, way) in osm_data.ways.iter() {
164        let mut coordinates: Vec<(f64, f64)> = Vec::new();
165        for node in way.nodes.iter() {
166            match osm_data.resolve_reference(&node) {
167                osm_xml::Reference::Node(n) => coordinates.push((n.lat, n.lon)),
168                osm_xml::Reference::Unresolved  |
169                osm_xml::Reference::Way(_)      |
170                osm_xml::Reference::Relation(_) => {
171                    warn!("Found a non-node as part of way {}'s node list: {:?}", way.id, node);
172                }
173            }
174        }
175        if way.is_polygon() { // Polygon
176            let poly: gt::Polygon<f64> = gt::Polygon::new(coordinates.into(), vec![]);
177            let geo_tile = Arc::new(polygon_feature_to_geo_tile(&way.tags, poly.clone()));
178            draw_polygon(&poly, geo_tile, data_structure.clone());
179        } else { // LineString
180            let line_string: gt::LineString<f64> = coordinates.into();
181            let geo_tile = Arc::new(line_string_feature_to_geo_tile(&way.tags, line_string));
182            draw_line_string(geo_tile, data_structure.clone());
183        }
184    }
185    // Relations
186    // TODO: INCOMPLETE - not sure how to handle this scenario yet.
187}
188
189pub fn process_pbf_with_data_structure(pbf_data: &BTreeMap<OsmId, OsmObj>, data_structure: GeoTilesDataStructure) {
190    for obj in pbf_data.values() {
191        let mut tags = obj.tags().clone();
192        tags.insert("id".to_string(), obj.id().inner_id().to_string());
193        match obj {
194            OsmObj::Node(obj) => {
195                let point: gt::Point<f64> = (obj.lat(), obj.lon()).try_into().unwrap();
196                let geo_tile = Arc::new(point_feature_to_geo_tile(&tags, point));
197                draw_point(&point, geo_tile, data_structure.clone());
198            }
199            OsmObj::Way(obj) => {
200                let coordinates = obj.get_coordinates(&pbf_data);
201                if obj.is_open() { // LineString
202                    let line_string: gt::LineString<f64> = coordinates.into();
203                    let geo_tile = Arc::new(line_string_feature_to_geo_tile(&tags, line_string));
204                    draw_line_string(geo_tile, data_structure.clone());
205                } else { // Polygon
206                    let poly: gt::Polygon<f64> = gt::Polygon::new(coordinates.into(), vec![]);
207                    let geo_tile = Arc::new(polygon_feature_to_geo_tile(&tags, poly.clone()));
208                    draw_polygon(&poly, geo_tile, data_structure.clone());
209                }
210            }
211            OsmObj::Relation(_obj) => {
212                //let coordinates = obj.get_coordinates(&pbf_data, &mut vec![]);
213                // TODO: INCOMPLETE - not sure how to handle this scenario yet.
214            }
215        }
216    }
217}
218
219fn process_feature(
220    properties: &dyn GeoTileProperties,
221    geometry: &gj::Geometry,
222    data_structure: GeoTilesDataStructure,
223) {
224    match geometry.value {
225        gj::Value::Polygon(_) => {
226            let poly: gt::Polygon<f64> =
227                TryInto::<gt::Polygon<f64>>::try_into(geometry.value.clone()).unwrap();
228            let geo_tile = Arc::new(polygon_feature_to_geo_tile(properties, poly.clone()));
229            draw_polygon(&poly, geo_tile, data_structure);
230        }
231        gj::Value::MultiPolygon(_) => {
232            let multi_polygon: gt::MultiPolygon<f64> =
233                TryInto::<gt::MultiPolygon<f64>>::try_into(geometry.value.clone()).unwrap();
234            for polygon in multi_polygon {
235                let poly: gt::Polygon<f64> =
236                    TryInto::<gt::Polygon<f64>>::try_into(polygon).unwrap();
237                let geo_tile = Arc::new(polygon_feature_to_geo_tile(properties, poly.clone()));
238                draw_polygon(&poly, geo_tile, data_structure.clone());
239            }
240        }
241        gj::Value::GeometryCollection(ref gc) => {
242            for geom in gc {
243                process_feature(properties, geom, data_structure.clone())
244            }
245        }
246        gj::Value::LineString(_) => {
247            let line_string: gt::LineString<f64> =
248                TryInto::<gt::LineString<f64>>::try_into(geometry.value.clone()).unwrap();
249            let geo_tile = Arc::new(line_string_feature_to_geo_tile(properties, line_string));
250            draw_line_string(geo_tile, data_structure);
251        }
252        gj::Value::MultiLineString(_) => {
253            let multi_line_string: gt::MultiLineString<f64> =
254                TryInto::<gt::MultiLineString<f64>>::try_into(geometry.value.clone()).unwrap();
255            for line_string in multi_line_string {
256                let line_string: gt::LineString<f64> =
257                    TryInto::<gt::LineString<f64>>::try_into(line_string).unwrap();
258                let geo_tile = Arc::new(line_string_feature_to_geo_tile(properties, line_string));
259                draw_line_string(geo_tile, data_structure.clone());
260            }
261        }
262        gj::Value::Point(_) => {
263            let point: gt::Point<f64> =
264                TryInto::<gt::Point<f64>>::try_into(geometry.value.clone()).unwrap();
265            let geo_tile = Arc::new(point_feature_to_geo_tile(properties, point));
266            draw_point(&point, geo_tile, data_structure);
267        }
268        gj::Value::MultiPoint(_) => {
269            let multi_point: gt::MultiPoint<f64> =
270                TryInto::<gt::MultiPoint<f64>>::try_into(geometry.value.clone()).unwrap();
271            for point in multi_point {
272                let point: gt::Point<f64> = TryInto::<gt::Point<f64>>::try_into(point).unwrap();
273                let geo_tile = Arc::new(point_feature_to_geo_tile(properties, point));
274                draw_point(&point, geo_tile, data_structure.clone());
275            }
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::geojson_parser;
284
285    #[test]
286    fn test_parse_and_process_geojson_file() {
287        let geojson = geojson_parser::parse_geojson_file("resources/ottawa.xml.geojson");
288        process_geojson(&geojson);
289    }
290}