use crate::GraphWriter;
use rdftk_core::model::graph::GraphRef;
use rdftk_core::model::statement::{ObjectNodeRef, SubjectNodeRef};
use std::cell::RefCell;
use std::collections::HashMap;
use std::io::Write;
#[derive(Debug)]
pub struct DotOptions {
pub blank_shape: String,
pub blank_color: String,
pub blank_labels: bool,
pub iri_shape: String,
pub iri_color: String,
pub literal_shape: String,
pub literal_color: String,
pub node_prefix: String,
}
#[derive(Debug)]
pub struct DotWriter {
nodes: RefCell<HashMap<String, Node>>,
options: DotOptions,
}
#[derive(Debug)]
enum NodeKind {
Blank,
IRI,
Literal,
}
#[derive(Debug)]
struct Node {
id: String,
kind: NodeKind,
label: String,
}
impl Default for DotOptions {
fn default() -> Self {
Self {
blank_shape: "circle".to_string(),
blank_color: "green".to_string(),
blank_labels: false,
iri_shape: "ellipse".to_string(),
iri_color: "blue".to_string(),
literal_shape: "record".to_string(),
literal_color: "black".to_string(),
node_prefix: "node_".to_string(),
}
}
}
impl Default for DotWriter {
fn default() -> Self {
Self {
nodes: Default::default(),
options: Default::default(),
}
}
}
impl GraphWriter for DotWriter {
fn write(&self, w: &mut impl Write, graph: &GraphRef) -> rdftk_core::error::Result<()> {
writeln!(w, "digraph {{\n rankdir=BT\n charset=\"utf-8\";").map_err(io_error)?;
writeln!(w).map_err(io_error)?;
let graph = graph.borrow();
let mappings = graph.prefix_mappings();
for statement in graph.statements() {
writeln!(
w,
" \"{}{}\" -> \"node_{}\" [label=\"{}\"];",
self.options.node_prefix,
self.subject_id(statement.subject()),
self.object_id(statement.object()),
match mappings.borrow().compress(&statement.predicate()) {
None => statement.predicate().to_string(),
Some(qname) => qname.to_string(),
}
)
.map_err(io_error)?;
}
writeln!(w).map_err(io_error)?;
for node in self.nodes.borrow().values() {
match node.kind {
NodeKind::Blank => {
if self.options.blank_labels {
writeln!(
w,
" \"{}{}\" [label=\"{}{}\",shape={},color={}];",
self.options.node_prefix,
node.id,
self.options.node_prefix,
node.id,
self.options.blank_shape,
self.options.blank_color
)
.map_err(io_error)?;
} else {
writeln!(
w,
" \"{}{}\" [label=\"\",shape={},color={}];",
self.options.node_prefix,
node.id,
self.options.blank_shape,
self.options.blank_color
)
.map_err(io_error)?;
}
}
NodeKind::IRI => {
writeln!(
w,
" \"{}{}\" [URL=\"{}\",label=\"{}\",shape={},color={}];",
self.options.node_prefix,
node.id,
node.label,
node.label,
self.options.iri_shape,
self.options.iri_color
)
.map_err(io_error)?;
}
NodeKind::Literal => {
writeln!(
w,
" \"{}{}\" [label=\"{}\",shape={},color={}];",
self.options.node_prefix,
node.id,
node.label,
self.options.literal_shape,
self.options.literal_color
)
.map_err(io_error)?;
}
}
}
writeln!(w, "}}").map_err(io_error)?;
Ok(())
}
}
impl DotWriter {
pub fn new(options: DotOptions) -> Self {
Self {
nodes: Default::default(),
options,
}
}
fn subject_id(&self, node: &SubjectNodeRef) -> String {
let mut nodes = self.nodes.borrow_mut();
if let Some(node) = nodes.get(&node.to_string()) {
node.id.clone()
} else {
let id = format!("{}", nodes.len() + 1);
if node.is_blank() {
let _ = nodes.insert(
node.to_string(),
Node {
id: id.clone(),
kind: NodeKind::Blank,
label: node.as_blank().unwrap().clone(),
},
);
} else if node.is_iri() {
let _ = nodes.insert(
node.to_string(),
Node {
id: id.clone(),
kind: NodeKind::IRI,
label: node.as_iri().unwrap().to_string(),
},
);
}
id
}
}
fn object_id(&self, node: &ObjectNodeRef) -> String {
let mut nodes = self.nodes.borrow_mut();
if let Some(node) = nodes.get(&node.to_string()) {
node.id.clone()
} else {
let id = format!("{}", nodes.len() + 1);
if node.is_blank() {
let _ = nodes.insert(
node.to_string(),
Node {
id: id.clone(),
kind: NodeKind::Blank,
label: node.as_blank().unwrap().clone(),
},
);
} else if node.is_iri() {
let _ = nodes.insert(
node.to_string(),
Node {
id: id.clone(),
kind: NodeKind::IRI,
label: node.as_iri().unwrap().to_string(),
},
);
} else if node.is_literal() {
let _ = nodes.insert(
node.to_string(),
Node {
id: id.clone(),
kind: NodeKind::Literal,
label: node.as_literal().unwrap().lexical_form().clone(),
},
);
}
id
}
}
}
fn io_error(e: std::io::Error) -> rdftk_core::error::Error {
use rdftk_core::error::ErrorKind;
rdftk_core::error::Error::with_chain(e, ErrorKind::ReadWrite(super::NAME.to_string()))
}