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