use super::{PLOT_MARGIN, axis::format_value, count, topicmap::push_sea};
use crate::{
error::ChartWarning,
metrics::TextMetrics,
scene::{Circle, Element, Polyline, Scene, Text, TextAnchor, TextStyle},
spec::{AtlasSpec, ChartSpec, NumberStyle},
};
pub(crate) const REALM_CLASSES: [&str; 8] = [
"chartlet-atlas-realm-0",
"chartlet-atlas-realm-1",
"chartlet-atlas-realm-2",
"chartlet-atlas-realm-3",
"chartlet-atlas-realm-4",
"chartlet-atlas-realm-5",
"chartlet-atlas-realm-6",
"chartlet-atlas-realm-7",
];
pub(super) fn layout_atlas(
spec: &ChartSpec,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) -> Scene {
let atlas = spec
.atlas
.as_ref()
.expect("validated atlas charts carry an atlas block");
let margin = f64::from(PLOT_MARGIN);
let top = 78.0;
let plot = (
margin,
top,
f64::from(spec.width) - margin * 2.0,
f64::from(spec.height) - top - margin,
);
let landscape = crate::atlas::landscape(atlas, plot);
let mut elements = Vec::new();
push_sea(&mut elements, plot, false);
push_landscape(&mut elements, atlas, &landscape, spec.number_style());
push_places(&mut elements, &landscape);
push_names(&mut elements, atlas, &landscape, metrics, warnings);
Scene {
width: spec.width,
height: spec.height,
elements,
}
}
const PLACE_RADIUS: f64 = 2.2;
const OUTLINE_TOLERANCE: f64 = 0.25;
struct Taken {
x: f64,
y: f64,
half_width: f64,
half_height: f64,
}
fn clear_of(taken: &[Taken], want: &Taken) -> bool {
taken.iter().all(|other| {
(want.x - other.x).abs() >= want.half_width + other.half_width
|| (want.y - other.y).abs() >= want.half_height + other.half_height
})
}
fn push_places(elements: &mut Vec<Element>, landscape: &crate::atlas::Landscape) {
for spot in &landscape.spots {
let detail = spot
.place
.tooltip
.as_ref()
.map(|line| format!("{} · {line}", spot.place.label));
elements.push(Element::Circle(Circle {
cx: spot.x,
cy: spot.y,
radius: PLACE_RADIUS * spot.place.weight.sqrt(),
class: "chartlet-atlas-place",
topic: Some(spot.region),
series_index: None,
style_index: None,
tooltip: Some(detail.unwrap_or_else(|| spot.place.label.clone())),
}));
}
}
fn push_name(
elements: &mut Vec<Element>,
taken: &mut Vec<Taken>,
field: &crate::atlas::Field,
name: (&str, &'static str, Option<usize>),
room: (f64, f64),
inside: &dyn Fn(usize, usize) -> bool,
metrics: &impl TextMetrics,
) -> bool {
let (text, class, topic) = name;
let (smallest, largest) = room;
let depth = crate::contour::depth(field.columns, field.rows, inside);
let apart = (field.columns / 10).max(3);
for (column, row, deep) in crate::contour::roomiest(&depth, field.columns, apart, 4) {
let reach = f64::from(deep) * field.cell;
let (x, y) = field.centre(column, row);
let mut size = (reach * 0.7).clamp(smallest, largest);
while size > smallest && metrics.width(text, size) > reach * 1.8 {
size -= 1.0;
}
if metrics.width(text, size) > reach * 1.8 {
continue;
}
let want = Taken {
x,
y,
half_width: metrics.width(text, size) / 2.0 + 2.0,
half_height: size * 0.6,
};
if !clear_of(taken, &want) {
continue;
}
elements.push(Element::StyledText(
Text {
x,
y: y + size * 0.34,
class,
anchor: TextAnchor::Middle,
content: text.to_owned(),
},
TextStyle {
size: Some(size),
topic,
},
));
taken.push(want);
return true;
}
false
}
fn push_names(
elements: &mut Vec<Element>,
atlas: &AtlasSpec,
landscape: &crate::atlas::Landscape,
metrics: &impl TextMetrics,
warnings: &mut Vec<ChartWarning>,
) {
let field = &landscape.field;
let mut taken: Vec<Taken> = Vec::new();
for (realm, entry) in atlas.realms.iter().enumerate() {
push_name(
elements,
&mut taken,
field,
(&entry.label, "chartlet-atlas-realm-label", None),
(12.0, 32.0),
&|column, row| {
field
.at(column, row)
.is_some_and(|region| landscape.sites[region].realm == realm)
},
metrics,
);
}
let mut held = vec![0usize; landscape.sites.len()];
for owner in field.owner.iter().flatten() {
held[*owner] += 1;
}
let mut order: Vec<usize> = (0..landscape.sites.len()).collect();
crate::sort::by(&mut order, |a, b| held[*b].cmp(&held[*a]).then(a.cmp(b)));
let ranges = crate::atlas::realm_ranges(atlas);
for region in order {
let site = &landscape.sites[region];
let written = push_name(
elements,
&mut taken,
field,
(&site.region.label, "chartlet-atlas-label", Some(region)),
(9.0, 19.0),
&|column, row| field.at(column, row) == Some(region),
metrics,
);
if !written {
let realm = site.realm;
warnings.push(ChartWarning::new(
"label_does_not_fit",
format!(
"/atlas/realms/{realm}/regions/{}/label",
region - ranges[realm].start
),
format!(
"{:?} has no room for its own name at this size; the area keeps its tooltip",
site.region.label
),
));
}
}
let declared: usize = atlas
.realms
.iter()
.flat_map(|realm| &realm.regions)
.map(|region| region.places.len())
.sum();
if landscape.spots.len() < declared {
warnings.push(ChartWarning::new(
"places_did_not_fit",
"/atlas/realms",
format!(
"{} of {declared} places {} drawn; a region has more places than it has ground",
landscape.spots.len(),
if landscape.spots.len() == 1 {
"was"
} else {
"were"
}
),
));
}
}
fn push_landscape(
elements: &mut Vec<Element>,
atlas: &AtlasSpec,
landscape: &crate::atlas::Landscape,
style: NumberStyle,
) {
let field = &landscape.field;
let corner = |(column, row): (usize, usize)| {
(
field.origin.0 - field.cell / 2.0 + count(column) * field.cell,
field.origin.1 - field.cell / 2.0 + count(row) * field.cell,
)
};
let shapes = |inside: &dyn Fn(usize, usize) -> bool| -> Vec<Vec<(f64, f64)>> {
crate::contour::rings(field.columns, field.rows, inside)
.iter()
.map(|ring| {
let points: Vec<(f64, f64)> = ring.iter().map(|at| corner(*at)).collect();
let mut drawn = crate::contour::simplify(
&crate::contour::smooth(&crate::contour::straighten(&points)),
field.cell * OUTLINE_TOLERANCE,
);
if let Some(first) = drawn.first().copied() {
drawn.push(first);
}
drawn
})
.collect()
};
for (realm, entry) in atlas.realms.iter().enumerate() {
for points in shapes(&|column, row| {
field
.at(column, row)
.is_some_and(|region| landscape.sites[region].realm == realm)
}) {
elements.push(Element::Polyline(Polyline {
points,
class: REALM_CLASSES[realm],
topic: None,
series_index: None,
style_index: None,
tooltip: entry.tooltip.clone().or_else(|| Some(entry.label.clone())),
}));
}
}
if atlas.contours {
for level in &landscape.relief.levels {
for points in shapes(&|column, row| {
let cell = row * field.columns + column;
field.owner[cell].is_some() && landscape.relief.height[cell] >= *level
}) {
elements.push(Element::Polyline(Polyline {
points,
class: "chartlet-atlas-contour",
topic: None,
series_index: None,
style_index: None,
tooltip: None,
}));
}
}
}
for (region, site) in landscape.sites.iter().enumerate() {
for points in shapes(&|column, row| field.at(column, row) == Some(region)) {
elements.push(Element::Polyline(Polyline {
points,
class: "chartlet-atlas-region",
topic: Some(region),
series_index: None,
style_index: None,
tooltip: Some(site.region.tooltip.clone().unwrap_or_else(|| {
format!(
"{}: {}",
site.region.label,
format_value(site.region.value, style)
)
})),
}));
}
}
for points in shapes(&|column, row| field.at(column, row).is_some()) {
elements.push(Element::Polyline(Polyline {
points,
class: "chartlet-atlas-coast",
topic: None,
series_index: None,
style_index: None,
tooltip: None,
}));
}
}