1use std::collections::HashMap;
12
13use crate::geo::{GeoApplyTarget, apply_geo_features};
14use crate::{CoordinateSpace, ElementKey, GeoApplyReport, GeoFeature, GeoGeometry, GeoLayer};
15use powerio_dist::MulticonductorNetwork;
16
17#[must_use]
23pub fn to_dist_geo_layer(net: &MulticonductorNetwork) -> GeoLayer {
24 let mut features = Vec::new();
25 for (row, bus) in net.buses().iter().enumerate() {
26 let Some(location) = bus.location else {
27 continue;
28 };
29 features.push(GeoFeature {
30 target: crate::GeoTarget::Bus,
31 key: ElementKey {
32 uid: Some(format!("buses:{row}")),
33 id: Some(bus.id.clone()),
34 name: Some(bus.id.clone()),
35 index: None,
36 },
37 geometry: GeoGeometry::Point([location.x, location.y]),
38 from: None,
39 to: None,
40 kind: location.kind.and_then(kind_to_balanced),
41 });
42 }
43 for (row, line) in net.lines().iter().enumerate() {
44 let Some(route) = &line.route else {
45 continue;
46 };
47 features.push(GeoFeature {
48 target: crate::GeoTarget::Branch,
49 key: ElementKey {
50 uid: Some(format!("lines:{row}")),
51 id: Some(line.name.clone()),
52 name: Some(line.name.clone()),
53 index: None,
54 },
55 geometry: GeoGeometry::LineString(
56 route.iter().map(|point| [point.x, point.y]).collect(),
57 ),
58 from: Some(line.bus_from.clone()),
59 to: Some(line.bus_to.clone()),
60 kind: None,
61 });
62 }
63 let meta = mirror::<_, crate::GeoMeta>(net.geo().as_ref());
64 GeoLayer {
65 space: meta
66 .as_ref()
67 .map_or(CoordinateSpace::Unknown, |geo| geo.space.clone()),
68 kind: meta.and_then(|geo| geo.kind),
69 features,
70 }
71}
72
73#[must_use]
75pub fn to_dist_geo_layer_report(net: &MulticonductorNetwork) -> (GeoLayer, Vec<crate::Diagnostic>) {
76 let diagnostics = if net.component_locations().is_empty() {
77 Vec::new()
78 } else {
79 vec![crate::Diagnostic::of(
80 &powerio_dist::diagnostics::codes::EMIT_DIST_COMPONENT_COORDINATES_DROPPED,
81 format!(
82 "{} component geographic point(s) cannot be expressed by the legacy GeoLayer; partial axes, datum and device identities remain in the typed network",
83 net.component_locations().len()
84 ),
85 )]
86 };
87 (to_dist_geo_layer(net), diagnostics)
88}
89
90pub fn apply_dist_geo_layer(net: &mut MulticonductorNetwork, layer: &GeoLayer) -> GeoApplyReport {
96 let mut target = DistApply {
97 buses: DistBusIndex::new(net),
98 lines: DistLineIndex::new(net),
99 net,
100 };
101 let report = apply_geo_features(layer, &mut target);
102 if report.matched_buses > 0 || report.matched_branches > 0 {
103 *net.geo_mut() = mirror(Some(&crate::GeoMeta {
104 space: layer.space.clone(),
105 kind: layer.kind,
106 }));
107 }
108 report
109}
110
111struct DistApply<'a> {
113 net: &'a mut MulticonductorNetwork,
114 buses: DistBusIndex,
115 lines: DistLineIndex,
116}
117
118impl GeoApplyTarget for DistApply<'_> {
119 fn bus_row(&self, key: &ElementKey) -> Option<usize> {
120 key.uid
121 .as_ref()
122 .and_then(|uid| self.buses.rows.get(uid))
123 .or_else(|| lookup_lower(&self.buses.rows, key.id.as_deref()))
124 .or_else(|| lookup_lower(&self.buses.rows, key.name.as_deref()))
125 .copied()
126 }
127
128 fn branch_row(&self, feature: &GeoFeature) -> Option<usize> {
129 feature
130 .key
131 .uid
132 .as_ref()
133 .and_then(|uid| self.lines.rows.get(uid).copied())
134 .or_else(|| {
135 lookup_lower(&self.lines.rows, feature.key.id.as_deref())
136 .or_else(|| lookup_lower(&self.lines.rows, feature.key.name.as_deref()))
137 .copied()
138 })
139 .or_else(|| {
140 feature
142 .key
143 .index
144 .and_then(|index| index.checked_sub(1))
145 .filter(|row| *row < self.net.lines().len())
146 })
147 .or_else(|| {
148 let from = feature.from.as_deref()?;
149 let to = feature.to.as_deref()?;
150 self.lines.pairs.get(&name_pair(from, to)).copied()
151 })
152 }
153
154 fn place_bus(&mut self, row: usize, point: [f64; 2], kind: Option<crate::CoordsKind>) {
155 self.net.buses_mut()[row].location = Some(powerio_dist::DistLocation {
156 x: point[0],
157 y: point[1],
158 kind: kind.and_then(kind_to_dist),
159 });
160 }
161
162 fn place_branch(&mut self, row: usize, path: &[[f64; 2]], kind: Option<crate::CoordsKind>) {
163 let kind = kind.and_then(kind_to_dist);
164 self.net.lines_mut()[row].route = Some(
165 path.iter()
166 .map(|[x, y]| powerio_dist::DistLocation { x: *x, y: *y, kind })
167 .collect(),
168 );
169 }
170
171 fn substation_note(&self, count: usize) -> String {
172 format!(
173 "{count} substation feature(s) not applied: the multiconductor model has no \
174 substation join"
175 )
176 }
177
178 fn unlocated_counts(&self) -> (usize, usize) {
179 (
180 self.net
181 .buses()
182 .iter()
183 .filter(|bus| bus.location.is_none())
184 .count(),
185 self.net
186 .lines()
187 .iter()
188 .filter(|line| line.route.is_none())
189 .count(),
190 )
191 }
192}
193
194struct DistBusIndex {
196 rows: HashMap<String, usize>,
197}
198
199impl DistBusIndex {
200 fn new(net: &MulticonductorNetwork) -> Self {
201 let mut rows = HashMap::new();
202 for (row, bus) in net.buses().iter().enumerate() {
203 rows.insert(format!("buses:{row}"), row);
204 rows.entry(bus.id.to_ascii_lowercase()).or_insert(row);
205 }
206 Self { rows }
207 }
208}
209
210struct DistLineIndex {
213 rows: HashMap<String, usize>,
214 pairs: HashMap<(String, String), usize>,
215}
216
217impl DistLineIndex {
218 fn new(net: &MulticonductorNetwork) -> Self {
219 let mut rows = HashMap::new();
220 let mut pairs = HashMap::new();
221 for (row, line) in net.lines().iter().enumerate() {
222 rows.insert(format!("lines:{row}"), row);
223 rows.entry(line.name.to_ascii_lowercase()).or_insert(row);
224 pairs
225 .entry(name_pair(&line.bus_from, &line.bus_to))
226 .or_insert(row);
227 }
228 Self { rows, pairs }
229 }
230}
231
232fn lookup_lower<'a>(rows: &'a HashMap<String, usize>, key: Option<&str>) -> Option<&'a usize> {
233 rows.get(&key?.to_ascii_lowercase())
234}
235
236fn name_pair(a: &str, b: &str) -> (String, String) {
237 let a = a.to_ascii_lowercase();
238 let b = b.to_ascii_lowercase();
239 if b < a { (b, a) } else { (a, b) }
240}
241
242fn kind_to_balanced(kind: powerio_dist::DistCoordsKind) -> Option<crate::CoordsKind> {
246 match kind {
247 powerio_dist::DistCoordsKind::Source => Some(crate::CoordsKind::Source),
248 powerio_dist::DistCoordsKind::Synthetic => Some(crate::CoordsKind::Synthetic),
249 powerio_dist::DistCoordsKind::Manual => Some(crate::CoordsKind::Manual),
250 powerio_dist::DistCoordsKind::Derived => Some(crate::CoordsKind::Derived),
251 _ => mirror(Some(&kind)),
252 }
253}
254
255fn kind_to_dist(kind: crate::CoordsKind) -> Option<powerio_dist::DistCoordsKind> {
256 match kind {
257 crate::CoordsKind::Source => Some(powerio_dist::DistCoordsKind::Source),
258 crate::CoordsKind::Synthetic => Some(powerio_dist::DistCoordsKind::Synthetic),
259 crate::CoordsKind::Manual => Some(powerio_dist::DistCoordsKind::Manual),
260 crate::CoordsKind::Derived => Some(powerio_dist::DistCoordsKind::Derived),
261 _ => mirror(Some(&kind)),
262 }
263}
264
265fn mirror<S: serde::Serialize, T: serde::de::DeserializeOwned>(value: Option<&S>) -> Option<T> {
269 serde_json::to_value(value?)
270 .ok()
271 .and_then(|json| serde_json::from_value(json).ok())
272}