Skip to main content

tzf_rs/
geojson.rs

1//! GeoJSON export types and conversions (feature `export-geojson`).
2//!
3//! Coordinates are converted from the 1e5-scaled int32 storage through `f32`,
4//! matching the precision of the v1 export path, so output stays comparable
5//! across releases.
6
7use crate::finder::Item;
8use crate::tzb::{ExpandedPolygon, TileId};
9use serde::{Deserialize, Serialize};
10
11pub type PolygonCoordinates = Vec<Vec<[f64; 2]>>;
12pub type MultiPolygonCoordinates = Vec<PolygonCoordinates>;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct GeometryDefine {
16    #[serde(rename = "type")]
17    pub geometry_type: String,
18    pub coordinates: MultiPolygonCoordinates,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct PropertiesDefine {
23    pub tzid: String,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct FeatureItem {
28    #[serde(rename = "type")]
29    pub feature_type: String,
30    pub properties: PropertiesDefine,
31    pub geometry: GeometryDefine,
32}
33
34impl FeatureItem {
35    /// Serializes to a JSON string. Kept as an inherent method for v1 API
36    /// compatibility.
37    #[allow(clippy::inherent_to_string)]
38    #[must_use]
39    pub fn to_string(&self) -> String {
40        serde_json::to_string(self).unwrap_or_default()
41    }
42
43    #[must_use]
44    pub fn to_string_pretty(&self) -> String {
45        serde_json::to_string_pretty(self).unwrap_or_default()
46    }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct BoundaryFile {
51    #[serde(rename = "type")]
52    pub collection_type: String,
53    pub features: Vec<FeatureItem>,
54}
55
56impl BoundaryFile {
57    /// Serializes to a JSON string. Kept as an inherent method for v1 API
58    /// compatibility.
59    #[allow(clippy::inherent_to_string)]
60    #[must_use]
61    pub fn to_string(&self) -> String {
62        serde_json::to_string(self).unwrap_or_default()
63    }
64
65    #[must_use]
66    pub fn to_string_pretty(&self) -> String {
67        serde_json::to_string_pretty(self).unwrap_or_default()
68    }
69}
70
71pub(crate) fn collection(features: Vec<FeatureItem>) -> BoundaryFile {
72    BoundaryFile {
73        collection_type: "FeatureCollection".to_string(),
74        features,
75    }
76}
77
78fn scaled_coord(v: i32) -> f64 {
79    #[allow(clippy::cast_possible_truncation)]
80    let narrowed = (f64::from(v) / 1e5) as f32;
81    f64::from(narrowed)
82}
83
84fn ring_coords<'a>(points: impl Iterator<Item = &'a geometry_rs::Point<i32>>) -> Vec<[f64; 2]> {
85    points
86        .map(|p| [scaled_coord(p.x), scaled_coord(p.y)])
87        .collect()
88}
89
90/// Like [`ring_coords`], but for the open rings the `.tzb` expansion yields:
91/// GeoJSON rings must be closed, so the first coordinate is repeated at the
92/// end (matching the closed storage the materialized finder exports).
93fn ring_coords_closed<'a>(
94    points: impl Iterator<Item = &'a geometry_rs::Point<i32>>,
95) -> Vec<[f64; 2]> {
96    let mut coords = ring_coords(points);
97    if let Some(&first) = coords.first() {
98        coords.push(first);
99    }
100    coords
101}
102
103/// Builds one GeoJSON Feature from a finder item's materialized polygons.
104pub(crate) fn feature_from_item(item: &Item) -> FeatureItem {
105    let coordinates = item
106        .polys
107        .iter()
108        .map(|poly| {
109            let mut rings = PolygonCoordinates::new();
110            rings.push(ring_coords(poly.exterior().iter()));
111            for hole in poly.holes() {
112                rings.push(ring_coords(hole.iter()));
113            }
114            rings
115        })
116        .collect();
117    feature(item.name.clone(), coordinates)
118}
119
120/// Builds one GeoJSON Feature from rings expanded out of a `.tzb` file.
121pub(crate) fn feature_from_expanded(name: String, polys: &[ExpandedPolygon]) -> FeatureItem {
122    let coordinates = polys
123        .iter()
124        .map(|poly| {
125            let mut rings = PolygonCoordinates::new();
126            rings.push(ring_coords_closed(poly.exterior.iter()));
127            for hole in &poly.holes {
128                rings.push(ring_coords_closed(hole.iter()));
129            }
130            rings
131        })
132        .collect();
133    feature(name, coordinates)
134}
135
136/// Builds one GeoJSON Feature from FUZZY preindex tile keys: a MultiPolygon
137/// holding each tile's bounding rectangle as one closed ring.
138pub(crate) fn feature_from_tile_keys(name: String, keys: &[u64]) -> FeatureItem {
139    let coordinates = keys
140        .iter()
141        .map(|&key| vec![TileId(key).polygon()])
142        .collect();
143    feature(name, coordinates)
144}
145
146fn feature(name: String, coordinates: MultiPolygonCoordinates) -> FeatureItem {
147    FeatureItem {
148        feature_type: "Feature".to_string(),
149        properties: PropertiesDefine { tzid: name },
150        geometry: GeometryDefine {
151            geometry_type: "MultiPolygon".to_string(),
152            coordinates,
153        },
154    }
155}