radial sugiyama positioning integration
This commit is contained in:
200
radial_sugiyama/src/ttl.rs
Normal file
200
radial_sugiyama/src/ttl.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::path::Path;
|
||||
|
||||
use oxrdf::vocab::rdfs;
|
||||
use oxrdf::{NamedOrBlankNode, Term};
|
||||
use oxttl::{TurtleParseError, TurtleParser};
|
||||
|
||||
use crate::model::{Edge, Graph, Node};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TtlImportError {
|
||||
Io(std::io::Error),
|
||||
Parse(TurtleParseError),
|
||||
NoSubclassTriples,
|
||||
}
|
||||
|
||||
impl Display for TtlImportError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TtlImportError::Io(error) => write!(f, "failed to read Turtle input: {error}"),
|
||||
TtlImportError::Parse(error) => write!(f, "failed to parse Turtle input: {error}"),
|
||||
TtlImportError::NoSubclassTriples => {
|
||||
write!(
|
||||
f,
|
||||
"no usable rdfs:subClassOf triples were found in the Turtle input"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for TtlImportError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
TtlImportError::Io(error) => Some(error),
|
||||
TtlImportError::Parse(error) => Some(error),
|
||||
TtlImportError::NoSubclassTriples => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for TtlImportError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
Self::Io(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TurtleParseError> for TtlImportError {
|
||||
fn from(error: TurtleParseError) -> Self {
|
||||
Self::Parse(error)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn graph_from_ttl_reader<R: Read>(reader: R) -> Result<Graph, TtlImportError> {
|
||||
let mut nodes = Vec::new();
|
||||
let mut node_indices = HashMap::new();
|
||||
let mut edges = Vec::new();
|
||||
let mut seen_edges = HashSet::new();
|
||||
|
||||
for triple in TurtleParser::new().for_reader(reader) {
|
||||
let triple = triple?;
|
||||
if triple.predicate.as_ref() != rdfs::SUB_CLASS_OF {
|
||||
continue;
|
||||
}
|
||||
|
||||
let NamedOrBlankNode::NamedNode(subject) = triple.subject else {
|
||||
continue;
|
||||
};
|
||||
let Term::NamedNode(object) = triple.object else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let subclass = get_or_insert_node(&mut nodes, &mut node_indices, subject.as_str());
|
||||
let superclass = get_or_insert_node(&mut nodes, &mut node_indices, object.as_str());
|
||||
if seen_edges.insert((superclass, subclass)) {
|
||||
edges.push(Edge::new(superclass, subclass));
|
||||
}
|
||||
}
|
||||
|
||||
if edges.is_empty() {
|
||||
return Err(TtlImportError::NoSubclassTriples);
|
||||
}
|
||||
|
||||
Ok(Graph::new(nodes, edges))
|
||||
}
|
||||
|
||||
pub fn graph_from_ttl_path(path: impl AsRef<Path>) -> Result<Graph, TtlImportError> {
|
||||
let file = File::open(path)?;
|
||||
graph_from_ttl_reader(BufReader::new(file))
|
||||
}
|
||||
|
||||
fn get_or_insert_node(
|
||||
nodes: &mut Vec<Node>,
|
||||
node_indices: &mut HashMap<String, usize>,
|
||||
iri: &str,
|
||||
) -> usize {
|
||||
if let Some(&index) = node_indices.get(iri) {
|
||||
return index;
|
||||
}
|
||||
|
||||
let index = nodes.len();
|
||||
nodes.push(Node {
|
||||
label: Some(iri.to_owned()),
|
||||
..Node::default()
|
||||
});
|
||||
node_indices.insert(iri.to_owned(), index);
|
||||
index
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{layout_radial_hierarchy, LayoutConfig};
|
||||
|
||||
const TTL_PREFIXES: &str = "@prefix ex: <http://example.com/> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix schema: <http://schema.org/> .\n";
|
||||
|
||||
#[test]
|
||||
fn imports_only_subclass_triples() {
|
||||
let ttl = format!(
|
||||
"{TTL_PREFIXES}ex:A rdfs:subClassOf ex:B .\nex:A schema:name \"Alpha\" .\nex:B schema:name \"Beta\" .\n"
|
||||
);
|
||||
|
||||
let graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();
|
||||
|
||||
assert_eq!(graph.nodes.len(), 2);
|
||||
assert_eq!(graph.edges, vec![Edge::new(1, 0)]);
|
||||
assert_eq!(
|
||||
graph.nodes[0].label.as_deref(),
|
||||
Some("http://example.com/A")
|
||||
);
|
||||
assert_eq!(
|
||||
graph.nodes[1].label.as_deref(),
|
||||
Some("http://example.com/B")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deduplicates_repeated_subclass_triples() {
|
||||
let ttl =
|
||||
format!("{TTL_PREFIXES}ex:A rdfs:subClassOf ex:B .\nex:A rdfs:subClassOf ex:B .\n");
|
||||
|
||||
let graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();
|
||||
|
||||
assert_eq!(graph.edges, vec![Edge::new(1, 0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_blank_node_and_literal_targets() {
|
||||
let ttl = format!(
|
||||
"{TTL_PREFIXES}ex:A rdfs:subClassOf [ a ex:Anonymous ] .\nex:B rdfs:subClassOf \"Literal\" .\nex:C rdfs:subClassOf ex:D .\n"
|
||||
);
|
||||
|
||||
let graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();
|
||||
|
||||
assert_eq!(graph.nodes.len(), 2);
|
||||
assert_eq!(graph.edges, vec![Edge::new(1, 0)]);
|
||||
assert_eq!(
|
||||
graph.nodes[0].label.as_deref(),
|
||||
Some("http://example.com/C")
|
||||
);
|
||||
assert_eq!(
|
||||
graph.nodes[1].label.as_deref(),
|
||||
Some("http://example.com/D")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports_can_flow_into_layout() {
|
||||
let ttl =
|
||||
format!("{TTL_PREFIXES}ex:A rdfs:subClassOf ex:B .\nex:B rdfs:subClassOf ex:C .\n");
|
||||
|
||||
let mut graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();
|
||||
layout_radial_hierarchy(&mut graph, LayoutConfig::default()).unwrap();
|
||||
|
||||
assert!(graph
|
||||
.nodes
|
||||
.iter()
|
||||
.all(|node| node.x.is_finite() && node.y.is_finite()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_clear_error_for_invalid_turtle() {
|
||||
let ttl = "@prefix ex: <http://example.com/> .\nex:A rdfs:subClassOf .\n";
|
||||
let error = graph_from_ttl_reader(ttl.as_bytes()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, TtlImportError::Parse(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_clear_error_when_no_subclass_triples_exist() {
|
||||
let ttl = format!("{TTL_PREFIXES}ex:A schema:name \"Alpha\" .\n");
|
||||
let error = graph_from_ttl_reader(ttl.as_bytes()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, TtlImportError::NoSubclassTriples));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user