1use std::collections::HashMap;
14
15use serde_json::Value;
16
17use super::layer::{ElementKey, GeoApplyReport, GeoFeature, GeoGeometry, GeoLayer, GeoTarget};
18use super::{Canvas, CoordinateSpace, GeoMeta, Location};
19use crate::format::PwdDisplay;
20use crate::format::powerworld::AuxFile;
21use crate::network::BalancedNetwork;
22
23pub const PWD_MERCATOR_K: f64 = 535.816_08;
26
27#[must_use]
32pub fn pwd_mercator_to_lonlat(x: f64, y: f64) -> (f64, f64) {
33 let lon = x / PWD_MERCATOR_K;
34 let lat = ((y / PWD_MERCATOR_K).to_radians().sinh())
35 .atan()
36 .to_degrees();
37 (lon, lat)
38}
39
40#[must_use]
43pub fn geo_layer_from_pwd(display: &PwdDisplay) -> GeoLayer {
44 GeoLayer {
45 space: CoordinateSpace::Diagram {
46 canvas: Some(Canvas {
47 width: Some(f64::from(display.canvas_width)),
48 height: Some(f64::from(display.canvas_height)),
49 units: None,
50 }),
51 },
52 kind: None,
53 features: display
54 .substations
55 .iter()
56 .map(|substation| {
57 substation_feature(
58 substation_key(&substation.number.to_string()),
59 (!substation.name.is_empty()).then(|| substation.name.clone()),
60 [substation.x, substation.y],
61 )
62 })
63 .collect(),
64 }
65}
66
67#[must_use]
75pub fn geo_layer_from_aux_substations(aux: &AuxFile) -> GeoLayer {
76 let mut features = Vec::new();
77 for object in aux.data_of("Substation") {
78 let (Some(number), Some(latitude), Some(longitude)) = (
79 object
80 .field_index("SubNum")
81 .or_else(|| object.field_index("Number")),
82 object.field_index("Latitude"),
83 object.field_index("Longitude"),
84 ) else {
85 continue;
86 };
87 for row in &object.rows {
88 let field = |column: usize| -> Option<(&str, f64)> {
89 let text = row.values.get(column)?.trim();
90 let value = text.parse::<f64>().ok().filter(|value| value.is_finite())?;
91 Some((text, value))
92 };
93 let (Some((number, _)), Some((_, lat)), Some((_, lon))) =
94 (field(number), field(latitude), field(longitude))
95 else {
96 continue;
97 };
98 features.push(substation_feature(substation_key(number), None, [lon, lat]));
99 }
100 }
101 GeoLayer {
102 space: CoordinateSpace::Geographic { crs: None },
103 kind: None,
104 features,
105 }
106}
107
108pub fn apply_substation_points(net: &mut BalancedNetwork, layer: &GeoLayer) -> GeoApplyReport {
114 let mut report = GeoApplyReport::default();
115 let mut rows_by_substation: HashMap<String, Vec<usize>> = HashMap::new();
117 for (row, bus) in net.buses.iter().enumerate() {
118 if let Some(substation) = bus_substation(bus) {
119 rows_by_substation.entry(substation).or_default().push(row);
120 }
121 }
122 let mut replaced = 0usize;
123 for feature in &layer.features {
124 let (GeoTarget::Substation, GeoGeometry::Point(point)) =
125 (&feature.target, &feature.geometry)
126 else {
127 continue;
128 };
129 let rows = feature
130 .key
131 .id
132 .as_deref()
133 .and_then(|number| rows_by_substation.get(number));
134 let Some(rows) = rows else {
135 report.unmatched_features += 1;
136 continue;
137 };
138 for &row in rows {
139 let bus = &mut net.buses[row];
140 if bus.location.is_some() {
141 replaced += 1;
142 }
143 bus.location = Some(Location {
144 x: point[0],
145 y: point[1],
146 kind: feature.kind,
147 });
148 report.matched_buses += 1;
149 }
150 }
151 if report.matched_buses > 0 {
152 if replaced > 0 {
153 report
154 .notes
155 .push(format!("replaced {replaced} existing bus location(s)"));
156 }
157 super::layer::note_space_change(&mut report, net.geo.as_ref(), &layer.space);
158 net.geo = Some(GeoMeta {
159 space: layer.space.clone(),
160 kind: layer.kind,
161 });
162 }
163 (report.unlocated_buses, report.unlocated_branches) = super::layer::unlocated_counts(net);
164 report
165}
166
167fn bus_substation(bus: &crate::network::Bus) -> Option<String> {
170 let value = bus
171 .extras
172 .get("SubNum")
173 .or_else(|| bus.extras.get("SubNumber"))?;
174 match value {
175 Value::Number(number) => Some(substation_key(&number.to_string())),
176 Value::String(text) => {
177 let trimmed = text.trim();
178 (!trimmed.is_empty()).then(|| substation_key(trimmed))
179 }
180 _ => None,
181 }
182}
183
184fn substation_feature(id: String, name: Option<String>, point: [f64; 2]) -> GeoFeature {
187 GeoFeature {
188 target: GeoTarget::Substation,
189 key: ElementKey {
190 uid: None,
191 id: Some(id),
192 name,
193 index: None,
194 },
195 geometry: GeoGeometry::Point(point),
196 from: None,
197 to: None,
198 kind: None,
199 }
200}
201
202fn substation_key(number: &str) -> String {
206 number
207 .parse::<f64>()
208 .ok()
209 .filter(|v| v.fract() == 0.0 && v.abs() < 1e15)
210 .map_or_else(|| number.to_owned(), |v| format!("{v:.0}"))
211}