use std::collections::HashMap;
use serde_json::Value;
use super::layer::{ElementKey, GeoApplyReport, GeoFeature, GeoGeometry, GeoLayer, GeoTarget};
use super::{Canvas, CoordinateSpace, GeoMeta, Location};
use crate::format::PwdDisplay;
use crate::format::powerworld::AuxFile;
use crate::network::BalancedNetwork;
pub const PWD_MERCATOR_K: f64 = 535.816_08;
#[must_use]
pub fn pwd_mercator_to_lonlat(x: f64, y: f64) -> (f64, f64) {
let lon = x / PWD_MERCATOR_K;
let lat = ((y / PWD_MERCATOR_K).to_radians().sinh())
.atan()
.to_degrees();
(lon, lat)
}
#[must_use]
pub fn geo_layer_from_pwd(display: &PwdDisplay) -> GeoLayer {
GeoLayer {
space: CoordinateSpace::Diagram {
canvas: Some(Canvas {
width: Some(f64::from(display.canvas_width)),
height: Some(f64::from(display.canvas_height)),
units: None,
}),
},
kind: None,
features: display
.substations
.iter()
.map(|substation| {
substation_feature(
substation_key(&substation.number.to_string()),
(!substation.name.is_empty()).then(|| substation.name.clone()),
[substation.x, substation.y],
)
})
.collect(),
}
}
#[must_use]
pub fn geo_layer_from_aux_substations(aux: &AuxFile) -> GeoLayer {
let mut features = Vec::new();
for object in aux.data_of("Substation") {
let (Some(number), Some(latitude), Some(longitude)) = (
object
.field_index("SubNum")
.or_else(|| object.field_index("Number")),
object.field_index("Latitude"),
object.field_index("Longitude"),
) else {
continue;
};
for row in &object.rows {
let field = |column: usize| -> Option<(&str, f64)> {
let text = row.values.get(column)?.trim();
let value = text.parse::<f64>().ok().filter(|value| value.is_finite())?;
Some((text, value))
};
let (Some((number, _)), Some((_, lat)), Some((_, lon))) =
(field(number), field(latitude), field(longitude))
else {
continue;
};
features.push(substation_feature(substation_key(number), None, [lon, lat]));
}
}
GeoLayer {
space: CoordinateSpace::Geographic { crs: None },
kind: None,
features,
}
}
pub fn apply_substation_points(net: &mut BalancedNetwork, layer: &GeoLayer) -> GeoApplyReport {
let mut report = GeoApplyReport::default();
let mut rows_by_substation: HashMap<String, Vec<usize>> = HashMap::new();
for (row, bus) in net.buses().iter().enumerate() {
if let Some(substation) = bus_substation(bus) {
rows_by_substation.entry(substation).or_default().push(row);
}
}
let mut replaced = 0usize;
for feature in &layer.features {
let (GeoTarget::Substation, GeoGeometry::Point(point)) =
(&feature.target, &feature.geometry)
else {
continue;
};
let rows = feature
.key
.id
.as_deref()
.and_then(|number| rows_by_substation.get(number));
let Some(rows) = rows else {
report.unmatched_features += 1;
continue;
};
for &row in rows {
let bus = &mut net.buses_mut()[row];
if bus.location.is_some() {
replaced += 1;
}
bus.location = Some(Location {
x: point[0],
y: point[1],
kind: feature.kind,
});
report.matched_buses += 1;
}
}
if report.matched_buses > 0 {
if replaced > 0 {
report
.notes
.push(format!("replaced {replaced} existing bus location(s)"));
}
super::layer::note_space_change(&mut report, net.geo().as_ref(), &layer.space);
*net.geo_mut() = Some(GeoMeta {
space: layer.space.clone(),
kind: layer.kind,
});
}
(report.unlocated_buses, report.unlocated_branches) = super::layer::unlocated_counts(net);
report
}
fn bus_substation(bus: &crate::network::Bus) -> Option<String> {
let value = bus
.extras
.get("SubNum")
.or_else(|| bus.extras.get("SubNumber"))?;
match value {
Value::Number(number) => Some(substation_key(&number.to_string())),
Value::String(text) => {
let trimmed = text.trim();
(!trimmed.is_empty()).then(|| substation_key(trimmed))
}
_ => None,
}
}
fn substation_feature(id: String, name: Option<String>, point: [f64; 2]) -> GeoFeature {
GeoFeature {
target: GeoTarget::Substation,
key: ElementKey {
uid: None,
id: Some(id),
name,
index: None,
},
geometry: GeoGeometry::Point(point),
from: None,
to: None,
kind: None,
}
}
fn substation_key(number: &str) -> String {
number
.parse::<f64>()
.ok()
.filter(|v| v.fract() == 0.0 && v.abs() < 1e15)
.map_or_else(|| number.to_owned(), |v| format!("{v:.0}"))
}