1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// A GeoFRED / Maps shape file (the `geofred/shapes/file` endpoint) — the region
/// boundary polygons for a [`ShapeType`](crate::ShapeType), as a GeoJSON
/// `FeatureCollection`.
///
/// This crate **transports** GeoJSON without interpreting it (ADR-0025):
/// per-feature `properties` vary by shape type and the geometry coordinates are
/// in GeoFRED's own display projection (integer pixel-like pairs, not lat/lon),
/// so those parts are carried as [`serde_json::Value`] rather than modelled down
/// to the polygon. A consumer that needs typed, projected geometry can re-parse
/// the geometry with a dedicated GeoJSON crate. The `type`/`crs` fields are kept
/// so the value round-trips as valid GeoJSON.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ShapeFile {
/// The GeoJSON object type — `"FeatureCollection"`.
#[serde(rename = "type")]
pub kind: String,
/// FRED's name for the shape set, e.g. `"state_bea_region"`.
pub name: String,
/// The GeoJSON coordinate reference system, carried verbatim (kept for a
/// faithful round-trip; absent for shapes that omit it).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub crs: Option<Value>,
/// The region features — one boundary per region.
pub features: Vec<Feature>,
}
/// A single GeoJSON feature in a [`ShapeFile`]: a region's properties and its
/// boundary geometry.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Feature {
/// The GeoJSON object type — `"Feature"`.
#[serde(rename = "type")]
pub kind: String,
/// The region's properties, carried untyped. Keys vary by shape type (e.g. a
/// `bea` shape carries `bea_region`/`bea_regi_1`), and FRED emits an empty
/// **array** `[]` — not `{}` — for a feature with no properties, so this is a
/// [`serde_json::Value`] rather than a map.
pub properties: Value,
/// The region's boundary geometry.
pub geometry: Geometry,
}
/// A GeoJSON geometry within a [`Feature`]. The coordinates are carried untyped
/// (they are deep nested arrays in GeoFRED's display projection); `kind` is the
/// GeoJSON geometry type, e.g. `"MultiPolygon"`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Geometry {
/// The GeoJSON geometry type, e.g. `"MultiPolygon"` or `"Polygon"`.
#[serde(rename = "type")]
pub kind: String,
/// The geometry coordinates, verbatim — nested arrays in GeoFRED's display
/// projection, not interpreted by this crate.
pub coordinates: Value,
}
#[cfg(test)]
mod tests {
use super::*;
const SHAPE_FILE: &str = r#"{
"type": "FeatureCollection",
"name": "state_bea_region",
"crs": {"type": "name", "properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"}},
"features": [
{
"type": "Feature",
"properties": {"bea_region": 8, "bea_regi_1": "Far West"},
"geometry": {"type": "MultiPolygon", "coordinates": [[[[1485, 2651], [1482, 2635]]]]}
}
]
}"#;
#[test]
fn parses_feature_collection() {
let shapes: ShapeFile = serde_json::from_str(SHAPE_FILE).expect("shape file parses");
assert_eq!(shapes.kind, "FeatureCollection");
assert_eq!(shapes.name, "state_bea_region");
assert_eq!(shapes.features.len(), 1);
let feature = &shapes.features[0];
assert_eq!(feature.kind, "Feature");
assert_eq!(feature.properties["bea_regi_1"], Value::from("Far West"));
assert_eq!(feature.geometry.kind, "MultiPolygon");
}
#[test]
fn empty_properties_array_parses() {
// FRED emits `[]` (not `{}`) for a feature with no properties, and can
// return non-polygon geometries like MultiLineString — both must parse.
let feature: Feature = serde_json::from_str(
r#"{"type":"Feature","properties":[],
"geometry":{"type":"MultiLineString","coordinates":[[[-707,5188],[3651,2950]]]}}"#,
)
.expect("empty-properties feature parses");
assert_eq!(feature.properties, Value::Array(vec![]));
assert_eq!(feature.geometry.kind, "MultiLineString");
}
#[test]
fn round_trips_geometry_verbatim() {
// The untyped geometry must survive a parse -> serialize cycle unchanged.
let shapes: ShapeFile = serde_json::from_str(SHAPE_FILE).unwrap();
let reserialized = serde_json::to_value(&shapes).unwrap();
let original: Value = serde_json::from_str(SHAPE_FILE).unwrap();
assert_eq!(reserialized, original);
}
}