use std::collections::{BTreeMap, BTreeSet};
use serde_json::{Map, Value};
use super::{ElisionReason, ViewError, ViewEvent, ViewSpec};
use crate::mmds::{Document, TEXT_EXTENSION_NAMESPACE};
use crate::views::evaluate::evaluate_view;
pub const VIEW_EXTENSION_NAMESPACE: &str = "org.mmdflux.view.v1";
const PROJECTION_KEY: &str = "projection";
const NODE_RANKS_KEY: &str = "node_ranks";
const EDGE_WAYPOINTS_KEY: &str = "edge_waypoints";
const LABEL_POSITIONS_KEY: &str = "label_positions";
pub fn project(
canonical: &Document,
spec: &ViewSpec,
) -> Result<(Document, Vec<ViewEvent>), ViewError> {
let evaluation = evaluate_view(canonical, spec)?;
let retained_subgraphs =
retained_subgraphs(canonical, &evaluation.subgraphs, &evaluation.nodes);
let mut output = canonical.clone();
output.nodes = canonical
.nodes
.iter()
.filter(|node| evaluation.nodes.contains(&node.id))
.cloned()
.collect();
output.edges = canonical
.edges
.iter()
.filter(|edge| {
evaluation.nodes.contains(&edge.source) && evaluation.nodes.contains(&edge.target)
})
.cloned()
.collect();
output.subgraphs = canonical
.subgraphs
.iter()
.filter(|subgraph| retained_subgraphs.contains(&subgraph.id))
.cloned()
.map(|mut subgraph| {
subgraph.children.retain(|child| {
evaluation.nodes.contains(child) || retained_subgraphs.contains(child)
});
subgraph
.concurrent_regions
.retain(|child| retained_subgraphs.contains(child));
subgraph
})
.collect();
project_extensions(&mut output, &evaluation.nodes);
let events = view_events(canonical, &evaluation.nodes, &retained_subgraphs);
Ok((output, events))
}
fn project_extensions(output: &mut Document, retained_nodes: &BTreeSet<String>) {
output.extensions.insert(
VIEW_EXTENSION_NAMESPACE.to_string(),
shared_coordinates_view_extension(),
);
if let Some(text_extension) = output.extensions.get_mut(TEXT_EXTENSION_NAMESPACE) {
prune_text_projection(text_extension, retained_nodes);
}
}
fn shared_coordinates_view_extension() -> Map<String, Value> {
let mut extension = Map::new();
extension.insert(
"layout_mode".to_string(),
Value::String("shared_coordinates".to_string()),
);
extension.insert(
"boundary_policy".to_string(),
Value::String("omit".to_string()),
);
extension
}
fn prune_text_projection(extension: &mut Map<String, Value>, retained_nodes: &BTreeSet<String>) {
let Some(Value::Object(projection)) = extension.get_mut(PROJECTION_KEY) else {
return;
};
if let Some(Value::Object(node_ranks)) = projection.get_mut(NODE_RANKS_KEY) {
node_ranks.retain(|node_id, _| retained_nodes.contains(node_id));
}
projection.remove(EDGE_WAYPOINTS_KEY);
projection.remove(LABEL_POSITIONS_KEY);
}
fn retained_subgraphs(
output: &Document,
explicit_subgraphs: &BTreeSet<String>,
retained_nodes: &BTreeSet<String>,
) -> BTreeSet<String> {
let parent_by_subgraph: BTreeMap<String, Option<String>> = output
.subgraphs
.iter()
.map(|subgraph| (subgraph.id.clone(), subgraph.parent.clone()))
.collect();
let mut retained = BTreeSet::new();
for subgraph in explicit_subgraphs {
add_subgraph_with_ancestors(subgraph, &parent_by_subgraph, &mut retained);
}
for node in &output.nodes {
if retained_nodes.contains(&node.id)
&& let Some(parent) = &node.parent
{
add_subgraph_with_ancestors(parent, &parent_by_subgraph, &mut retained);
}
}
retained
}
fn add_subgraph_with_ancestors(
subgraph: &str,
parent_by_subgraph: &BTreeMap<String, Option<String>>,
retained: &mut BTreeSet<String>,
) {
let mut current = Some(subgraph.to_string());
while let Some(id) = current {
if !retained.insert(id.clone()) {
break;
}
current = parent_by_subgraph.get(&id).and_then(Clone::clone);
}
}
fn view_events(
canonical: &Document,
retained_nodes: &BTreeSet<String>,
retained_subgraphs: &BTreeSet<String>,
) -> Vec<ViewEvent> {
let mut events = Vec::new();
for node in &canonical.nodes {
if !retained_nodes.contains(&node.id) {
events.push(ViewEvent::NodeLeftView {
id: node.id.clone(),
reason: ElisionReason::Excluded,
});
}
}
for subgraph in &canonical.subgraphs {
if !retained_subgraphs.contains(&subgraph.id) {
events.push(ViewEvent::SubgraphLeftView {
id: subgraph.id.clone(),
reason: ElisionReason::Excluded,
});
}
}
let mut edge_ordinals: BTreeMap<(String, String), usize> = BTreeMap::new();
for edge in &canonical.edges {
let key = (edge.source.clone(), edge.target.clone());
let ordinal = edge_ordinals.entry(key).or_default();
let current_ordinal = *ordinal;
*ordinal += 1;
if !(retained_nodes.contains(&edge.source) && retained_nodes.contains(&edge.target)) {
events.push(ViewEvent::EdgeElided {
source: edge.source.clone(),
target: edge.target.clone(),
ordinal: current_ordinal,
label: edge.label.clone(),
reason: ElisionReason::EndpointOutsideView,
});
}
}
events
}