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